Skip to content

fix: call dropped when user taps before app fully initializes#997

Merged
SERDUN merged 19 commits into
developfrom
fix/early-registration-guard-on-call-start
Mar 13, 2026
Merged

fix: call dropped when user taps before app fully initializes#997
SERDUN merged 19 commits into
developfrom
fix/early-registration-guard-on-call-start

Conversation

@SERDUN

@SERDUN SERDUN commented Mar 11, 2026

Copy link
Copy Markdown
Member

Summary

Fix outgoing call being dropped when the user taps call before the app fully initializes.

Two root causes: `CallController` was failing immediately when routing state was not yet loaded, and `CallBloc` was running the registration guard before the signaling wait completed.

Changes

  • `CallController` now waits for routing state instead of failing immediately; shows `NoInternetConnectionNotification` if no network after 10 s
  • `CallBloc.__onCallPerformEventStarted` — registration guard moved after signaling wait; signaling wait now fails fast on `isFailure` instead of waiting full timeout; fixed `forEach(async)` on `addTrack` that caused `createOffer` to run before tracks were added

Test plan

  • Open app and immediately tap call — proceeds once app finishes initializing
  • Normal call flow unchanged

This comment was marked as resolved.

@SERDUN SERDUN added the draft Not ready but can be start to review label Mar 12, 2026
SERDUN added 6 commits March 12, 2026 14:11
In __onCallPerformEventStarted, the early registration guard ran before
the signaling wait, causing calls to drop immediately when the user tapped
call before the socket initialized. Move the guard after the signaling wait
and extend the wait predicate to also require registration status to be
known (isHandshakeEstablished && isSignalingEstablished).
When the user taps call before CallRoutingCubit has initialized (app just
launched, user info not yet fetched), wait for the first non-null routing
state instead of immediately failing. The call proceeds automatically once
routing state becomes available. If the cubit is disposed while waiting,
the call is silently dropped.
Cover immediate dispatch, pending wait, cubit disposal, and
CallUndefinedLineNotification scenarios.
@SERDUN SERDUN force-pushed the fix/early-registration-guard-on-call-start branch from 570fd4f to 2ab0204 Compare March 12, 2026 12:12
SERDUN added 12 commits March 12, 2026 14:33
…ignaling failure

- Make createCall void by delegating to private _createCallAsync via unawaited, avoiding unawaited_futures lint at all call sites
- Add isFailure condition to signaling firstWhere predicate so outgoing calls fail immediately on signaling failure instead of waiting full timeout
Replace await controller.createCall(...) with call + await Future.delayed(Duration.zero)
to pump microtasks after createCall became void

This comment was marked as resolved.

@SERDUN SERDUN removed the draft Not ready but can be start to review label Mar 12, 2026
@SERDUN SERDUN requested a review from digiboridev March 12, 2026 14:44
@SERDUN SERDUN merged commit 35a5fb3 into develop Mar 13, 2026
1 check passed
@SERDUN SERDUN deleted the fix/early-registration-guard-on-call-start branch March 13, 2026 12:33
@SERDUN SERDUN mentioned this pull request Mar 31, 2026
SERDUN added a commit that referenced this pull request Apr 6, 2026
* fix: wait for signaling and registration before failing outgoing call

In __onCallPerformEventStarted, the early registration guard ran before
the signaling wait, causing calls to drop immediately when the user tapped
call before the socket initialized. Move the guard after the signaling wait
and extend the wait predicate to also require registration status to be
known (isHandshakeEstablished && isSignalingEstablished).

* fix: hold outgoing call as pending until routing state is available

When the user taps call before CallRoutingCubit has initialized (app just
launched, user info not yet fetched), wait for the first non-null routing
state instead of immediately failing. The call proceeds automatically once
routing state becomes available. If the cubit is disposed while waiting,
the call is silently dropped.

* fix: remove unused notification import from CallController

* test: add CallController.createCall unit tests

Cover immediate dispatch, pending wait, cubit disposal, and
CallUndefinedLineNotification scenarios.

* fix: remove unused optional params in _FakeCallRoutingState

* fix: remove unnecessary call_controller.dart import in test

* fix: address Copilot review — unawaited createCall and fast-fail on signaling failure

- Make createCall void by delegating to private _createCallAsync via unawaited, avoiding unawaited_futures lint at all call sites
- Add isFailure condition to signaling firstWhere predicate so outgoing calls fail immediately on signaling failure instead of waiting full timeout

* fix: update call_controller tests to use void createCall

Replace await controller.createCall(...) with call + await Future.delayed(Duration.zero)
to pump microtasks after createCall became void

* refactor: extract signaling wait predicate into named variables

* fix: use currentState instead of state for registration check after signaling wait

* fix: replace non-ASCII em dash with semicolon in comment

* refactor: remove redundant signalingConnected/registrationKnown vars, use CallState getters directly

* fix: await all addTrack calls before createOffer to prevent empty offer

* refactor: add TODO to provide CallController as singleton via RepositoryProvider

* fix: log warning when callRoutingCubit closes before routing state arrives

* refactor: add comment explaining callRoutingState await logic

* fix: add timeout to routing state wait and show NoInternetConnectionNotification on expiry

* refactor: extract _waitForRoutingState helper to clean up routing state await

* fix: catch unexpected errors from _createCallAsync and add timeout notification test
SERDUN added a commit that referenced this pull request Apr 28, 2026
* chore: disable overwrite and reviewed flags on upload (#892)

* fix: solve "sticky speaker" issue by localizing audio state (#895)

Moved `speakerOnBeforeMinimize` from global `CallState` to `ActiveCall`
model to ensure audio preferences are call-specific and properly
disposed of when a call ends.

- Fixes a bug where a new audio call would erroneously start on
  speakerphone if a previous call (or transfer session) had speaker
  enabled.
- Updated `CallBloc` to preserve and restore speaker state using
  the specific `ActiveCall` instance during minimization,
  transfers, and screen transitions.
- Cleaned up `CallState` to prevent stale audio state leakage
  between unrelated call sessions.

* fix: prevent duplicate outgoing calls using hybrid event transformer (#897)

* fix: change CallControlEvent transformer to droppable

* fix: use hybrid transformer for CallControlEvent to prevent duplicate calls

* fix: expand small touch target size for menu button (#894)

* fix: expand small touch target size for menu button

Fixed small tap target zone for popup menu button in voicemail tile

* refactor: improve voicemail menu button interaction and hover shape

Replaced the 'child' property with 'icon' in PopupMenuButton to fix the
square hover effect. This change ensures a standard Material circular
splash and restores the correct touch target size.

---------

Co-authored-by: Dmytro Serdun <d.serdun@webtrit.com>

* refactor: cache CallBloc to avoid unsafe context access (#899)

* refactor: cache CallBloc to avoid unsafe context access

* chore: sync project metadata and adjust initialization order

* chore: sync lockfiles and regenerate localization mappers (#900)

* fix: prioritize config color for avatar initials (#901)

Previously, the LeadingAvatarStyleFactory forcibly overrode the text color with `onSecondaryContainer`, ignoring the value from the configuration.

This change ensures the configuration color is used if provided, falling back to the theme default only when necessary.

* style: apply initials text color to loading and placeholder icons (#902)

* feat: support initial expansion state in MediaSettingsScreen (#903)

- Add initialOpenSection parameter to MediaSettingsScreen
- Implement MockMediaSettingsCubit for screenshot testing
- Register MediaSettingsScreenScreenshot in the screenshot router

* fix: media settings panel color (#906)

* style: set background color for expansion panels

* docs: add TODOs explaining the dark mode background override

* feat: implement ProgressIndicatorThemeData with Material 3 surface mapping (#907)

* fix: correct foreground color for app bar actions (#908)

* refactor(messaging): enhance GroupAvatar styling and logic (#910)

* chore(lefthook): move heavy checks to pre-push and add format check (#912)

* feat: implement declarative screen styling and theme override system (#911)

* feat: add theme override capabilities to ThemedScaffold

Introduces ContentThemeOverride to allow forcing light or dark modes
on the scaffold or its body independently.

- Add ContentThemeOverride enum (auto, light, dark).
- Add contentThemeOverride and ignoreAppBarOverride parameters.
- Implement _resolveThemeOverride to dynamically generate theme data.
- Update build logic to conditionally wrap the scaffold or body in a Theme widget.
- Refactor background decoration logic for better readability.

* refactor: replace Scaffold with ThemedScaffold across main feature screens

* chore: add theme override configurations and documentation to page configs

* feat: add theme override configuration for pages

* feat(theme): implement comprehensive screen theming and override system

Overhauls the screen styling architecture to support dynamic background styles
and explicit theme mode overrides (Light/Dark) across all main features.

- Implement ThemeOverrideConfig to manage theme mode forcing and AppBar
  synchronization.
- Introduce screen-specific styles (Contacts, Embedded, Favorites,
  Conversations, Recents) with background and theme override support.
- Refactor ThemedScaffold to use applyToAppBar (positive logic) for
  better readability and predictable theme propagation.
- Add StyleFactories for all migrated screens to map declarative
  JSON configurations to UI styles.
- Update BottomMenuTab models to include theme override data for
  tab-level consistency.
- Standardize MainAppBar behavior to handle complex backgrounds (gradients/images)
  by dynamically adjusting transparency and elevation.

* chore(theme): synchronize theme override keys and visibility in page configs

* fix: prevent content overlap with AppBar on complex backgrounds

* docs: align applyToAppBar documentation and default behavior

* fix: revert changes in bottom_menu_feature.dart (#913)

* fix: fixed formatting on about screen (#905)

* fix: fixed formatting on about screen

Transferred core url after version and divided app information and device information.

* refactor: optimize layout and decompose widgets

* feat: add documentation and enrich appInfo output in AppMetadataProvider

---------

Co-authored-by: Dmytro Serdun <d.serdun@webtrit.com>

* fix: fixed contacts ordering (#904)

Added collate method with no case option when sorting contacts

* feat: implement feature configuration system and unified dark theme support (#914)

* refactor: update dark theme configuration and enable mode support

* feat: implement supported features schema and unify theme mode

* feat: apply forced theme mode from FeatureAccess configuration

* refactor: swap page and widget theme configurations to correct files

* refactor: standardize theme mode types and naming (#915)

* refactor: rename ThemeModeConfig.auto to ThemeModeConfig.system

* refactor: replace custom ContentThemeOverride with standard ThemeMode

* refactor: rename toContentThemeOverride to toThemeMode

* fix: correctly handle nullable contentThemeOverride

* refactor: utilize ThemeProvider in ThemedScaffold for consistent overrides (#916)

* refactor: rename ThemeModeConfig.auto to ThemeModeConfig.system

* refactor: replace custom ContentThemeOverride with standard ThemeMode

* refactor: use ThemeProvider for theme overrides

Replaces the manual theme generation logic in ThemedScaffold with
direct retrieval from ThemeProvider. This ensures that when a theme
mode is forced (Light/Dark), the resulting ThemeData includes all
custom extensions and component styles defined in the app.

* fix: support dark mode in screenshot generation (#918)

* fix: remove hardcoded colors to allow context-based inheritance (#917)

* refactor: migrate login screens to ThemedScaffold and enhance styling models (#919)

* fix: polish theming and fix active call overlay (#920)

* style: remove temporary background color overrides in MediaSettingsScreen

* fix: wrap active thumbnail in Card for overlay support

The active call thumbnail is rendered inside an overlay, which is detached
from the main Scaffold tree. Without a Material ancestor, the widget lacks
theme context and elevation.

This change wraps the content in a Card to ensure proper Material styling
and shadows when displayed as a floating thumbnail.

* refactor(settings): use theme colors for microphone warning

* refactor: sync localizely keys and update plural logic (#921)

* refactor(app): implement safe teardown sequence for user logout (#922)

* refactor(app): implement safe teardown sequence for user logout

Introduces a dedicated `Teardown` application lifecycle state to ensure safe resource cleanup.

- Add `AppLifecycleStatus` to `AppBloc` to manage authentication states.
- Create `TeardownScreen` to hold the UI context while the `MainShell` unmounts.
- Implement `UserSessionCleanupResolver` to centralize repository and database clearing.
- Remove `onLogout` callback from `SessionRepository` and `main.dart`.
- Refactor `AppRouter` guards to handle the new teardown navigation flow.

* refactor: implement best-effort strategy for user session cleanup

* refactor: rename AppLogined to AppLoggedIn and refine logout documentation

* feat(autoprovision): pass system info when logging in via autoprovision

* refactor: implement memory-first session strategy and remove reactive streams

* fix: promote SessionRepository to a singleton in bootstrap

* refactor: implement intent-based logout orchestration in AppBloc

* style: optimize event instantiation and refine code comments

* style: reorder members in AppEvent classes

* feat: localize TeardownScreen progress text

* feat: explicitly handle storage cleanup during session updates

* refactor: refine teardown screen navigation guard

* docs: refine _saveSession documentation to match delegation pattern

* feat: Implement dynamic styling for LoginSwitch segmented button (#925)

* feat: add ButtonStyle and Geometry configuration models

* feat: implement configurable ButtonStyle for LoginSwitchScreen

* chore: add configurable SegmentedButton styling for login switch

* feat: add disabled state colors to ButtonStyleConfig

* feat: support disabled states and improve border side resolution in button styles

* refactor: migrate primary elevated button to ButtonStyleConfig

* refactor: convert CallPopupMenuButton to StatelessWidget and disable click effects (#923)

* refactor: replace custom tab buttons with standard TabBar and add unread badges (#924)

* refactor: replace custom tab buttons with standard TabBar and add unread badges

* refactor: remove redundant TabButtonsBar widget

* refactor: optimize tab selection logic and state updates

* fix(theme): propagate defaultFontFamily to theme factories (#926)

* fix(theme): propagate defaultFontFamily to theme factories

Updated theme style factories and configuration extensions to accept and apply
a `defaultFontFamily`. This ensures consistent font rendering across the app
by explicitly passing the font family derived from the global TextTheme into
custom component styles.

- Added `defaultFontFamily` parameter to `TextStyleConfig.toTextStyle` and
  related style conversion methods.
- Modified `ThemeStyleFactoryProvider` to initialize a `defaultTextTheme` and
  pass its font family to various screen and widget factories.
- Refactored multiple factories (Keypad, CallScreen, Login, Settings, etc.)
  to support the new font propagation logic.
- Replaced redundant `createTextTheme()` calls with a cached `defaultTextTheme`.

* refactor: improve initials text style mapping and reduce log verbosity

* fix(contacts): prevent race condition in LocalContactsSyncBloc during teardown (#927)

* refactor: re-engineer actionpad styling to semantic roles (#928)

* refactor: move ActionPadWidgetConfig from widget to page configuration

* refactor: add defaultVerticalAlignment to TextButtonsTable

* refactor(keypad): transition Actionpad to semantic button styling

Redesign Actionpad styling logic to move away from function-specific
identifiers toward semantic roles. This decoupling allows for more
flexible UI configurations and cleaner style inheritance.

* fix: correct button style merging and disabled states in ActionPad

* fix(call): increase monitor interval to optimize log quota (#929)

* refactor: remove hardcoded timeouts in PeerConnectionManager

* refactor: increase RtpTrafficMonitor check interval to 15s

Update the default monitorCheckInterval from 2s to 15s to optimize log
storage and prevent quota exhaustion. This adjustment reduces log
volume by approximately 85% while maintaining sufficient granularity
for monitoring network quality trends.

* feat: enhance RemoteConfigService with snapshots and stream support (#930)

* refactor: consolidate remote config service implementations

* refactor: enhance remote config service and caching logic

* refactor: move remote config service to services directory

* refactor: rename FirebaseRemoteConfigService to CachedRemoteConfigService

* feat: add remote config update stream support

* feat: integrate remote config overrides into FeatureAccess

* refactor: implement Snapshot pattern for RemoteConfigService

* refactor: update RemoteConfigService architecture and snapshot handling

* refactor: improve FeatureAccess reactivity and StreamUtils robustness

* refactor: add disposal guards to CachedRemoteConfigService and clean up redundant comments

* refactor: introduce FeatureAccessStreamFactory and decouple stream logic from RootApp

* refactor: update FeatureAccess architecture with specialized factories and rxdart integration

* refactor: ensure deterministic equality in CoreSupportImpl by sorting flags

* feat: dynamic RTP traffic monitoring interval and configuration synchronization (#931)

* feat: implement dynamic RTP monitoring configuration

- Added MonitoringConfig model and MonitoringMapper to handle monitor intervals.
- Updated PeerConnectionManager to support runtime configuration updates.
- Introduced CallConfigSynchronizer to reactively update CallBloc when FeatureAccess changes.
- Added 'monitorConfig' to SupportedFeature for static app configuration.
- Integrated Remote Config override for monitor check interval via 'feature_monitor_check_interval_sec'.
- Enabled voicemail settings by default in app.config.json.

* fix: align app.config.json key with SupportedMonitorConfig model

- Rename 'checkInterval' to 'checkIntervalSec' in app.config.json.
- Fixes an issue where the static monitor configuration was ignored due to a naming mismatch with the generated SupportedMonitorConfig model.
- Ensures the default 15-second interval is correctly read from the assets.

* fix(data): add monitoringConfig to FeatureAccess Equatable props

* fix(data): validate remote config monitor interval to prevent zero or negative values

* fix(call): allow disabling RTP monitor via zero interval

* test(data): verify monitor interval remote overrides and refactor validation

* docs: fix parameter name in SupportedFeature.monitorConfig doc comment

* fix: allow zero interval to propagate for RTP monitor disablement

* test(call): add unit tests for RtpTrafficMonitor

- Verified that checkInterval <= 0 successfully acts as a kill switch, preventing timer initialization and native calls.
- Verified that a valid positive interval correctly triggers periodic WebRTC stats fetching.
- Verified that monitoring automatically stops when RTCPeerConnection enters a closed state.
- Covered edge cases for zero and negative durations to ensure stability against invalid configurations.

* feat: generalize emergency reboot logic and refine diagnostic reporting (#932)

* feat: add reboot dialog

Added dialog for user to reboot user when timeout error on starting a call if it is Xiaomi phone

* refactor: replace AppMetadataProvider with DeviceInfo in DiagnosticService

* refactor: inject DeviceInfo into DiagnosticService in AppShell

* feat: include Huawei in emergency reboot diagnostic logic

* chore: update system error dialog localizations

* refactor(diagnostic): improve error handling and decouple UI logic

* refactor: simplify system error dialog and unify diagnostic launchers

* refactor: unify diagnostic flow and streamline reporting

* chore: remove trailing periods from system error dialog titles

---------

Co-authored-by: Alex Maudza <o.maudza@webtrit.com>

* refactor: add controller to HistoryAutocompleteField for manual history saving (#933)

* refactor: add controller to HistoryAutocompleteField for manual history saving

- Implement HistoryAutocompleteController to allow manual triggers for saving input to history.
- Convert LoginCoreUrlAssignScreen to a StatefulWidget to manage the controller lifecycle.
- Update HistoryAutocompleteField to support the new controller and internal logic extraction.
- Refactor callbacks into private methods and simplify widget building logic.

* fix: avoid redundant history saving in HistoryAutocompleteField

* feat: migrate system notifications and SIP presence to supported features (#934)

Refactor feature configuration by introducing systemNotifications and
sipPresence as variants of SupportedFeature. This change initiates
the transition away from legacy flat properties in AppConfigMain.

* fix: collect outbound rtp stats (#937)

* fix: description for Media encoding configs (#938)

Fixed description for Media encoding configs in Media settings. Full Flex option was removed, other options were renamed.

* fix(call): preserve widget context during auto-compact transition (#939)

Refactor CallActiveScaffold to use AnimatedOpacity and IgnorePointer
instead of conditional widget removal. This ensures that CallActions
remains in the widget tree, preventing callback failures (e.g.,
onAudioDeviceChanged) when the UI compacts while a popup is open.

* fix: fixed localization issues in network settings (#936)

Fixed issues with localization in network settings. Also added radio buttons and checkbox for 'Incoming Call Type' and 'SMS Fallback' accordingly

* chore: actualize l10n code generation (#940)

* chore: actualize configurations (#942)

* chore: implement modular rules system (#943)

* chore: implement modular rules system and copilot instructions (#946)

* chore: implement modular rules system and copilot instructions

* chore: add makefile helpers for copilot branch management

* feat: add theme switching settings (#951)

* chore: implement modular rules system and copilot instructions

* feat(settings): expose theme mode switching in settings

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>

* feat: remote cli settings (#944)

* feat: support metadata for missed call display name in isolate (#953)

* feat: support metadata for missed call display name in isolate

- Introduce getDisplayNameForMissedCall in IsolateManager to resolve caller names.
- Update PushNotificationIsolateManager to utilize CallkeepIncomingCallMetadata for missed call notifications.
- Pass metadata through onPushNotificationSyncCallback and onSignalingSyncCallback.
- Ensure LocalPushRepository returns the future from the notification plugin.

* docs: add documentation for IsolateManager properties

* refactor: improve missed call display name logic and logging

* feat(call): override _onNoActiveLines in PushNotificationIsolateManager for missed call handling (#959)

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: SERDUN <26858237+SERDUN@users.noreply.github.com>

* chore: ai agent pipeline refinement (#954)

* chore: ignore agent files

* docs: clarify commit message convention

* chore: enforce lowercase commit descriptions and update rules docs

* docs: refine AI agent execution pipeline and commit rules

* fix: user id migration (#960)

* refactor: replace SupportedMonitorConfig with SupportedLoggingConfig (#957)

* refactor: replace SupportedMonitorConfig with SupportedLoggingConfig

* refactor(logging): decouple AppLogger from RemoteConfigSnapshot

- make kLogLevelKey private in FeatureOverridesFactory
- add remoteLoggingEnabled to FeatureOverrides and LoggingConfig
- rewrite AppLogger.init() to accept LoggingConfig instead of RemoteConfigSnapshot
- add watchFeatureAccess() to observe loggingConfig changes via FeatureAccess stream
- add LoggingMapper.mapFromOverridesOnly() for isolate/background contexts
- update bootstrap.dart and services_isolate.dart call sites

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(logging): replace direct FeatureAccess stream with applyConfig in AppLogger

- remove watchFeatureAccess() and StreamSubscription from AppLogger
- add applyConfig(LoggingConfig) as the single public update point
- call applyConfig in App.didChangeDependencies() via existing FeatureAccess watch
- remove redundant feature_access.dart import from app_logger.dart

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(logging): narrow AppLogger.applyConfig to accept Level directly

- applyConfig now takes Level instead of LoggingConfig
- caller extracts loggingConfig.logLevel before passing to AppLogger

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(logging): remove LoggingConfig dependency from AppLogger

- init() now accepts Level and bool remoteLoggingEnabled as primitives
- remove models.dart import from app_logger.dart
- extract primitives from LoggingConfig at each call site

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(logging): remove EnvironmentConfig dependency from AppLogger

- init() now accepts logzioLogLevel and pre-built List<RemoteLoggingService>
- store logzioLogLevel as field for use in applyConfig
- move _buildRemoteLoggingServices to bootstrap.dart and services_isolate.dart
- remove environment_config.dart import from app_logger.dart

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(logging): simplify AppLogger by replacing List<RemoteLoggingService> with LogzioLoggingService?

- add LogzioLoggingService.fromEnvironment() factory to encapsulate EnvironmentConfig logic
- AppLogger.init() now accepts LogzioLoggingService? instead of List<RemoteLoggingService>
- remove _buildRemoteLoggingServices helpers from bootstrap.dart and services_isolate.dart

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(logging): move logzioLogLevel resolution into LogzioLoggingService.fromEnvironment

- fromEnvironment() now reads REMOTE_LOGZIO_LOG_LEVEL internally, no longer takes minLevel param
- AppLogger.init() drops logzioLogLevel parameter, uses _logzioService?.minLevel in applyConfig
- remove environment_config.dart import from services_isolate.dart

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(logging): add unit tests for LoggingMapper and FeatureAccessStreamFactory logging fields

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(call): compare monitorCheckInterval directly to avoid spurious CallBloc updates

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* chore: update Flutter to 3.41.2 and suppress experimental_member_use warnings (#962)

* chore: update Flutter to 3.41.2 and suppress experimental_member_use warnings

Both LockCachingAudioSource (just_audio) and TableMigration (drift) are
the only available APIs for their respective tasks with no stable
alternatives. Added ignore directives with explanatory comments.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: bump minimum Flutter SDK constraint to 3.41.2

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* chore: add claude code team settings (#965)

* chore: add Claude Code team settings and documentation

Add team-level `.claude/settings.json` with deny rules for keystores,
signing keys, and environment files. Add `.claude/settings.local.json`
to `.gitignore` and document settings levels in `docs/development.md`.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: add safe tool allow rules to team settings

Allow flutter test, analyze, dart format, dart fix, pub get, and
gen-l10n commands in team settings for all developers.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* fix: remove unused import in messaging_shell.dart (#966)

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* fix(settings): add optimistic toggle with spinner for register status switch (#967)

Replaces the blocking register status toggle with an optimistic update
pattern — the switch flips immediately, shows a spinner, disables during
the API call, and reverts on failure.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* fix: skip static payloads sanitizing if no rtpmap (#968)

* fix: media preset translation (#969)

* fix: fix splash config generation and add android_12 image support (#865)

- Escape # in Makefile color variables to prevent shell comment issues
- Remove redundant shell parameter expansion, use Make-only expansion
- Add ANDROID_12_SPLASH_IMAGE variable for separate Android 12 splash
- Use single quotes in echo to avoid shell interpretation problems

Co-authored-by: Dmytro Serdun <d.serdun@webtrit.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* chore: add diagnostic logging for recents number mismatch (#970)

Add hash-based and raw value logging to trace why recent calls list
shows two different phone numbers per entry. Raw values are visible
when Logz.io anonymization is disabled, hashes work with it enabled.

* feat: add support a blurred appbar surface (#971)

* feat: add BlurredSurface widget and apply blur effect to all app bars

Extract common ClipRect+BackdropFilter pattern into reusable BlurredSurface widget.
Apply consistent blur background with extendBodyBehindAppBar to all screens
using MainAppBar or AppBar. Remove isComplexBackground conditional logic.

* refactor: standardize MainAppBar field order across screens

* feat: apply blurred appbar surface to ConversationsScreen

* fix: remove unused theme/theme.dart imports

* fix: add top padding to prevent content overlap with blurred appbar

Screens using extendBodyBehindAppBar lacked compensating padding,
causing body content to be hidden behind the blurred AppBar. Added
appropriate top padding to recents, favorites, about, contacts, and
CDR screens consistent with the existing settings screen approach.

* feat(screenshots): make IgnorePointer configurable in ScreenshotApp (#972)

Add ignorePointer parameter (default true) to allow enabling interaction
for specific screenshots that require it.

* feat: add screenshot mocks and docs for all features (#973)

* feat: add screenshot mocks for all features

Add ~20 new screenshot definitions covering all user-visible screens:
- Core flows: contact, chat conversation, SMS conversation, system
  notifications, recent CDRs, number CDRs, call log
- Settings: network, language, diagnostic, caller ID, presence,
  theme mode, voicemail
- Login: switch screen
- Utility: log records console, contacts agreement, teardown,
  permissions, user agreement

Includes shared mock data, mock blocs/cubits/repositories,
screenshot widgets, router entries, and integration test registrations.

* docs: add screenshots package documentation

Add docs for screenshots architecture, mock reference, data reference,
and step-by-step guide for adding new screenshots. Update README with
links to the new documentation.

* fix: address review feedback for screenshot mocks and docs

- Add provider as direct dependency in screenshots/pubspec.yaml
  and remove ignore: depend_on_referenced_packages from 7 files
- Fix mock type/factory mismatches in mocks.md documentation
- Fix broken code formatting in mocks.md and adding_screenshots.md
- Remove shadowed ChatTypingCubit/SmsTypingCubit BlocProviders
  from conversation screenshot wrappers
- Guard registerFallbackValue with static flag to prevent
  duplicate registration
- Fix duplicated phrase in README.md Firebase Hosting section
- Document both flutter test and flutter drive workflows in
  overview.md

* feat: update keystores path to new applications subdirectory (#974)

* feat: add text style background decoration support (#975)

* feat: add backgroundBorderRadius and backgroundPadding to TextStyleConfig

* feat: add greetingTextStyle to LoginModeSelectPageConfig

* feat: add ExtendedText widget with background decoration support

* feat: add greetingTextStyle to login.modeSelect docs

* feat: add tests for ExtendedText widget

* feat: update theme template configs with missing components and fixes (#976)

- Fix calStatuses typo to callStatuses in both widget configs
- Add missing widget sections as examples: button, group, bar, input, text, confirmDialog
- Fix non-existent dark SVG asset references to use existing logos
- Remove unsupported fontFeatures from page text styles
- Clean up dark page config duplicate keys in login.switchPage
- Add greetingTextStyle for dark login modeSelect visibility
- Update dark widget decoration gradient config

* refactor: replace deprecated GradientColorsConfig with PageBackground (#977)

* refactor: replace deprecated GradientColorsConfig with PageBackground

Remove GradientColorsConfig, DecorationConfig, GradientsStyleFactory,
and Gradients theme extension. Migrate login and call screens to use
PageBackground via ThemedScaffold.

* refactor: enable gradient backgrounds for login and call page configs

Activate background for login modeSelect and add gradient background
to dialing section using colors from the removed GradientColorsConfig.

* refactor: adjust dark login gradient to contrast with buttons

Change bottom gradient color from #000000 to #0A1929 to avoid
blending with neutralOnDark button surface color.

* refactor: reverse dark login gradient direction

Start with dark color on top, lighter blue on bottom to keep
contrast with buttons at the bottom of the screen.

* refactor: rename misleading onSurface variable to surfaceColor

* feat: appBar blurred surface config and theme updates (#979)

* feat: extract appBar background color and blurred surface to config pipeline

Add configurable appBarBackgroundColor and appBarBlurredSurface (color, sigmaX, sigmaY)
to the theme config→style pipeline across all 7 main screens, replacing hardcoded values.

* feat: update page configuration docs with appBar fields

Add Common page fields section documenting appBarBackgroundColor
and appBarBlurredSurface, update Keypad page example.

* feat: remove appBarBackgroundColor from config pipeline

AppBar backgroundColor is already handled by MainAppBar fallback chain
(explicit → appBarTheme → canvasColor.withAlpha). Remove redundant
appBarBackgroundColor from BasePageConfig, all page configs, screen styles,
factories, and screens to avoid confusion.

* feat: add BlurredSurface.fromStyle factory and simplify screen usage

Replace inline BlurredSurface construction with fromStyle() factory
that returns null when no config is present (no blur applied) or a
configured BlurredSurface with resolved sigma defaults of 10.

* feat: enable appBar blur and activate widget config sections

- Add appBarBlurredSurface to all 7 page sections (dark/light)
- Activate previously ignored widget config sections (_bar, _button,
  _group, _input, _text -> remove underscore prefix)
- Set appBar backgroundColor/surfaceTintColor to transparent for blur
- Fix tab indicator label contrast and remove divider line

* feat: update light theme appBarBlurredSurface color to soft blue-white

* feat: set dark status bar icons for light theme appBar

* feat: fix appBarBlurredSurface sigma defaults and add tests

- Make sigmaX/sigmaY nullable in BlurredSurfaceConfig so omitted fields
  resolve to 10 via BlurredSurface.fromStyle instead of staying 0
- Regenerate Freezed/JSON for BlurredSurfaceConfig
- Restore blur on RecentCdrs and SystemNotifications screens by passing
  sigmaX: 10, sigmaY: 10 explicitly to const BlurredSurface()
- Explicit Colors.transparent fallback in BlurredSurface Container child
- Update docs to reflect null defaults with 10 resolved at widget layer
- Add BlurredSurface.fromStyle widget tests (4 cases)

* fix: system back button blocked in EmbeddedRequestErrorDialog (#935)

- Remove canPop: false from EmbeddedRequestErrorDialog — default is true,
  and blocking pop prevented the system back button from working in
  Settings → Terms & Conditions when the page failed to load (WT-890).
- Add onPopInvokedWithResult to call onBack when system back dismisses
  the dialog in Mode B (pushed route), ensuring _errorDialogShown is reset.
- Reset _errorDialogShown in _handleEmbeddedErrorState when error is
  cleared, so repeated errors after system-back dismissal are shown again.

Co-authored-by: Dmytro Serdun <d.serdun@webtrit.com>

* fix: prevent keepalive write-after-close and harden transaction lifecycle (#870)

* fix: prevent keepalive write-after-close and add regression test

* fix: clean up transaction on send failure in _executeTransaction

Move _addMessage inside the try-catch block so that if writing to a
closed socket throws WebtritSignalingBadStateException, the transaction
is properly removed from the _transactions map instead of leaking.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* test: add transaction cleanup and keepalive timer tests

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: add comment for closeCode guard in keepalive loop

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: address Copilot review comments on keepalive integration tests

- Fix compile error: replace called(greaterThan(0)) with called(1)
- Fix lint warning: await streamController.close() in tearDown
- Fix false-positive test: move closeCode mock before flushMicrotasks
  and add verify closeCode called(2) to catch timer-restart regression

---------

Co-authored-by: Dmytro Serdun <d.serdun@webtrit.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* fix: fixed app is locked in Bluetooth call profile after first call (#982)

* fix: fixed app is locked in Bluetooth call profile after first call

Fixed situation when user returns to YouTube or music playback, the audio remains degraded, like during a call. This happens only on Android.

* fix: improve comment for _onLastCallEnded to cover iOS and Android audio routing

---------

Co-authored-by: Dmytro Serdun <d.serdun@webtrit.com>

* feat: add melos v7 integration (#980)

* feat: add melos integration with codegen, test, format, and dependency scripts

* feat: fix melos scripts syntax for v7 and rename format to fmt

* feat: configure melos v7 with workspace and scripts in pubspec.yaml

- Add workspace: list to pubspec.yaml for Dart pub workspace / Melos v7 package discovery
- Add melos: section to pubspec.yaml with scripts for codegen, tests, formatting, dependencies
- Add resolution: workspace to all sub-package pubspec.yaml files
- Fix ssl_certificates publish_to typo (none; → 'none')
- Update screenshots SDK constraint to ^3.8.0
- Remove melos.yaml (ignored by Melos v7 — config lives in pubspec.yaml)

* feat: include root package in melos workspace via useRootAsPackage

* fix: replace non-ASCII em dashes with hyphens in pubspec.yaml comments

* feat: migrate makefile targets to melos scripts and mark deprecated

* fix: replace __ with _ in screenshots to fix unnecessary_underscores lint

* fix: rename melos script from run to start to avoid CLI conflict

The melos v7 CLI command `melos run <script>` conflicted with the script
named `run`. When running `melos run fmt`, melos treated `run` as the
script name and `fmt` as an argument, causing flutter to look for a file
named `fmt` instead of formatting code.

Renaming the script to `start` (and `run:ios` to `start:ios`) resolves
the ambiguity. Use `melos run start` to launch the app.

* feat: add melos smoke tests and update docs with melos commands

- Add tool/scripts/melos_smoke_test.sh for smoke testing safe scripts
- Add smoke:test melos script to pubspec.yaml
- Rename start to start:android for clarity
- Replace Makefile references with melos commands in docs/build.md
- Rewrite docs/make_file.md as full melos commands reference
- Update README.md to link to Melos Commands

* fix: address Copilot review comments

- Switch get/upgrade/outdated melos scripts from exec to run
  to correctly resolve from workspace root (not per-package)
- Align screenshots flutter constraint with workspace root (^3.41.2)
  to fix sdk/flutter constraint inconsistency (Dart 3.8 requires Flutter 3.41+)

* fix: reverse date divider in chat (#983)

Placed date divider before bunch of messages. Also changed displaying date in this divider. Added displaying options "Today", "Yesterday", day of week, like "Monday", and date in E, d MMM format, like "Tue, 6 Jan" , depending on comparison between date of message and today's date.

* feat: claude code setup (#981)

* feat: replace .rules with CLAUDE.md for Claude Code

* feat: configure .claude with hooks and allowed commands

* feat: add melos usage rules and update common commands

* feat: reduce CLAUDE.md duplication and soften melos usage rules

* feat: add webtrit_callkeep docs and package-level CLAUDE.md files

* feat: document call architecture, flows, isolates, and key patterns in CLAUDE.md

* feat: restructure AI agent memory files (AGENTS.md + CLAUDE.md split)

- Add root AGENTS.md (<100 lines): universal instructions for any AI tool
- Slim root CLAUDE.md (<50 lines): @imports + Claude-specific gotchas only
- Extract CallBloc architecture to docs/call_architecture.md
- Add AGENTS.md for all 10 packages (shared content, any agent)
- Remove package CLAUDE.md where content fully moved to AGENTS.md
- Keep package CLAUDE.md only where Claude-specific gotchas exist (data, ssl_certificates)
- Add CLAUDE.local.md to .gitignore

* feat: address Copilot review comments in PR #981

- Fix md_formatter.py docstring: was 'prettier', now correctly says 'markdownlint-cli2 --fix'
- Expand settings.json deny list to cover keystores (.jks, .keystore, .p12) and signing keys (.pem, .key, .p8)
- Fix AGENTS.md import groups: two '2.' entries corrected to sequential 1–6 numbering
- Fix packages/data/CLAUDE.md migration steps: misnumbered list corrected to 1–5
- Add working directory note to packages/data/AGENTS.md commands section
- Fix _web_socket_channel/AGENTS.md: 'pinned to' → 'constrained to' for caret constraint

* feat: remove webtrit_phone_keystores from deny list (dir is outside project scope)

* feat: configure gitignore, formatter, and analysis for generated files (#985)

* feat: configure gitignore, formatter, and analysis for generated files

- Add **/build/ and **/.claude/ to .gitignore
- Update lefthook pre-commit to skip *.g.dart / *.freezed.dart / *.gr.dart
- Replace melos fmt/fmt:check with find-based scripts that exclude generated files

* feat: update melos v7 integration across features, DAOs, and screenshots

* feat: allow feat/ as valid branch prefix alongside feature/ (#987)

* feat: allow feat/ as valid branch prefix alongside feature/

Update branch-name-check.sh and git-lint.yml to accept both feat/ and
feature/ prefixes. Add CONTRIBUTING.md documenting branch naming rules
and commit conventions.

* docs: fix table alignment in CONTRIBUTING.md

* docs: consolidate git conventions into CONTRIBUTING.md as single source of truth

- Remove .rules.md (referenced non-existent .rules/ directory)
- Update .github/copilot-instructions.md to reference CONTRIBUTING.md and AGENTS.md
- Add feat/ to branch pattern in copilot-instructions.md
- Fix pre-commit hook description in docs/development.md (dart format, not flutter analyze)
- Add CONTRIBUTING.md reference in docs/development.md
- Add docs/development.md pointer in CONTRIBUTING.md hooks section

* feat: favorites remote syncable (#984)

* refactor: cdr ui improvements, disconnect reason translations (#989)

* fix: use firstWhere instead of first in getAllContacts test to avoid ordering assumption (#991)

* fix: change contact_phones UNIQUE constraint to (number, label, contact_id) (#992)

* feat: change contact_phones UNIQUE constraint to (number, label, contact_id)

* feat: update ContactPhonesDao to use (number, label) pairs for stale-row deletion

* feat: persist per-label phone rows in ContactsLocalDataSource for DID-only accounts

* feat: add tests for DID-only contact phone handling and schema migration v20

* docs: add inline comment explaining grouping and merge logic in displayPhones

* refactor: rename lambda param p to phone in deleteOtherContactPhonesOfContactId call

* style: apply dart format to changed files

* docs: add DartDoc to deleteOtherContactPhonesOfContactId

* docs: add inline comments to deleteOtherContactPhonesOfContactId body

* style: replace non-ASCII arrow with hyphen in comment

* fix: use canonical label in displayPhones to prevent merged label from reaching favorites

* fix: recreate contact_phones via new table to preserve favorites FK reference

* test: add favorites FK integrity assertion for migration v20

* fix: resolve canonical ContactPhone by id before passing to favorites

* docs: document merged label display and canonical label favorites behavior

* refactor: replace ContactPhone display with flat fields in ContactPhoneDisplayEntry

- Replace synthetic ContactPhone display field with displayLabel (String)
  and displayFavorite (bool) - only the two fields that actually differ
  from the canonical phone
- Rename canonical field to phone for clarity
- Update displayPhones convenience getter to reconstruct ContactPhone
  from phone + flat display fields
- Update ContactPhoneTileAdapter to accept only primitives and closures,
  removing all model object dependencies
- Update ContactScreen loop to use displayPhoneEntries with closures
  capturing entry.phone directly
- Add widget tests for ContactPhoneTileAdapter covering all enable flags,
  callback wiring, transfer logic, and popup menu entries
- Add direct tests for displayPhoneEntries covering displayLabel,
  displayFavorite, and phone field contracts

* feat: melos exported env support (#993)

* fix: log records file stability improvements (#998)

* fix: guard File.delete() with exists() check in shareLogRecords

Prevents PathNotFoundException on Android 15 when the OS clears the
app cache directory under background memory pressure while the native
share sheet is open, causing the finally block to attempt deleting a
file that no longer exists (Crashlytics issue 25d22f4de9016c556b46cfacea29a20b).

* fix: delegate dispose Future in LogRecordsFileRepositoryImpl

Without awaiting or returning the Future from RotatingFileAppender.dispose(),
the file handle could remain unclosed after the caller awaited repository
disposal. Use direct return to delegate the Future without an async wrapper.

* fix: replace existsSync retry with async exists() in _getAllLogFilesWithRetry

existsSync() reads from the OS filesystem cache and can return false
immediately after forceFlush() even though the file is on disk.
Switching to async File.exists() forces a fresh stat() call that
bypasses the cache. Also reduces max wait from 10s (5x2s) to 1s (10x100ms).

* fix: guard forceFlush() with try/catch in readAllLogs

An I/O error during forceFlush() would propagate and abort readAllLogs
entirely, returning no logs to the caller. Catching the error allows
reading whatever files are already on disk.

* fix: guard forceFlush() with try/catch in cleanLogs

An I/O error during forceFlush() would abort cleanLogs entirely,
leaving stale log files on disk. Catching the error allows deletion
to proceed on whatever files are already present.

* fix: replace retry loop with async file discovery in log records

- Replace _getAllLogFilesWithRetry (10×100ms polling loop) with
  _getAllLogFilesAsync — single async pass using await file.exists()
  per rotation slot, bypassing OS filesystem cache without any delay
- Extend file discovery range to 0..keepRotateCount inclusive so
  rotated files (e.g. app_logs.log.1) are found even when base file
  is absent immediately after rotation
- Fix cleanLogs to use await _getAllLogFilesAsync() instead of
  synchronous getAllLogFiles() which relied on existsSync()
- Fix readAllLogs iteration order: remove files.reversed so newer
  file (rotation 0) is read before older rotated file (rotation 1)
- Remove redundant file.exists() guard inside readAllLogs loop since
  _getAllLogFilesAsync already guarantees file presence
- Add full test coverage: LogRecordsMemoryRepositoryImpl,
  ReadableRotatingFileAppender (readAllLogs + cleanLogs),
  LogRecordsFileRepositoryImpl — 26 tests total

* fix: update outdated comment in readAllLogs test

* feat: add Claude Code PostToolUse hooks (dart formatter + newline enforcer) (#995)

* fix: call dropped when user taps before app fully initializes (#997)

* fix: wait for signaling and registration before failing outgoing call

In __onCallPerformEventStarted, the early registration guard ran before
the signaling wait, causing calls to drop immediately when the user tapped
call before the socket initialized. Move the guard after the signaling wait
and extend the wait predicate to also require registration status to be
known (isHandshakeEstablished && isSignalingEstablished).

* fix: hold outgoing call as pending until routing state is available

When the user taps call before CallRoutingCubit has initialized (app just
launched, user info not yet fetched), wait for the first non-null routing
state instead of immediately failing. The call proceeds automatically once
routing state becomes available. If the cubit is disposed while waiting,
the call is silently dropped.

* fix: remove unused notification import from CallController

* test: add CallController.createCall unit tests

Cover immediate dispatch, pending wait, cubit disposal, and
CallUndefinedLineNotification scenarios.

* fix: remove unused optional params in _FakeCallRoutingState

* fix: remove unnecessary call_controller.dart import in test

* fix: address Copilot review — unawaited createCall and fast-fail on signaling failure

- Make createCall void by delegating to private _createCallAsync via unawaited, avoiding unawaited_futures lint at all call sites
- Add isFailure condition to signaling firstWhere predicate so outgoing calls fail immediately on signaling failure instead of waiting full timeout

* fix: update call_controller tests to use void createCall

Replace await controller.createCall(...) with call + await Future.delayed(Duration.zero)
to pump microtasks after createCall became void

* refactor: extract signaling wait predicate into named variables

* fix: use currentState instead of state for registration check after signaling wait

* fix: replace non-ASCII em dash with semicolon in comment

* refactor: remove redundant signalingConnected/registrationKnown vars, use CallState getters directly

* fix: await all addTrack calls before createOffer to prevent empty offer

* refactor: add TODO to provide CallController as singleton via RepositoryProvider

* fix: log warning when callRoutingCubit closes before routing state arrives

* refactor: add comment explaining callRoutingState await logic

* fix: add timeout to routing state wait and show NoInternetConnectionNotification on expiry

* refactor: extract _waitForRoutingState helper to clean up routing state await

* fix: catch unexpected errors from _createCallAsync and add timeout notification test

* fix: await reportNewIncomingCall in background FCM handler (#1001)

Without await the Pigeon IPC Future was fire-and-forget — the background
isolate was destroyed before the call reached the Kotlin side, so
PhoneConnectionService.startIncomingCall() was never invoked and the
incoming call UI never appeared.

* feat: provide CallController as singleton via RepositoryProvider in MainShell (#1000)

* feat: provide CallController as singleton via RepositoryProvider in MainShell

Register CallController once in MainShell widget tree after CallBloc,
CallRoutingCubit and NotificationsBloc are available. All seven call
sites now obtain the shared instance via late final field initializer
(context.read<CallController>()) instead of constructing a new object
per StatefulWidget.

* feat: replace RepositoryProvider with CallControllerScope InheritedWidget

RepositoryProvider is semantically for the data layer; CallController
is a UI-tier coordinator. Introduce CallControllerScope (InheritedWidget)
following the PresenceViewParams pattern already in the codebase.
All seven consumers now resolve the controller via CallControllerScope.of(context).

* fix: prevent stale CallController on MainShell rebuild

Store CallController in _MainShellState via ??= so it is created once
regardless of how many times build() reruns. Switch CallControllerScope.of
to getElementForInheritedWidgetOfExactType to avoid registering a rebuild
dependency that would never fire (updateShouldNotify is always false).

* feat: add doc comment for _callController field in _MainShellState

* fix: feature access mapper logic (#1007)

* fix: correct icon and text case assertions in integration tests (#1010)

* fix: correct icon and text case assertions in integration tests

* revert: remove accidentally committed logger from call_bloc.dart

* feat: add integration tests for webtrit_signaling keepalive and disconnect (#1009)

* feat: add integration tests for webtrit_signaling keepalive and disconnect

Add mock-based and live integration tests covering:
- Keepalive timeout (WebtritSignalingKeepaliveTransactionTimeoutException)
- Normal keepalive cycles (echo → timer restarts)
- Graceful disconnect (onDisconnect callback)
- Stream error (onError callback)
- Execute after disconnect (WebtritSignalingDisconnectedException)
- Request transaction timeout (non-keepalive variant)
- Live tests against a real server (skipped when credentials not set)
- Network simulation via pure-Dart WebSocket proxy with pause/resume
  to verify keepalive timeout fires on real packet drop

* feat: replace WebSocket proxy with raw TCP proxy in live signaling test

Drop packets at the TCP byte level instead of WebSocket frame level
for more realistic network simulation. The proxy rewrites the HTTP
Host header before forwarding so the server accepts the upgrade request.

* feat: extract TcpProxy into reusable _tcp_proxy package

Move the raw TCP proxy from the signaling test into a standalone
internal package packages/_tcp_proxy so it can be used as a
dev dependency in both webtrit_signaling tests and app-level tests.

* fix: address Copilot review comments on signaling tests and tcp proxy

- Fix expectLater() to pass Future directly instead of closure
- Make live test client nullable to prevent LateInitializationError in tearDown
- Use local signalingClient variable in each live test for clarity
- Replace localhost with 127.0.0.1 to avoid IPv6-first resolution issues
- Remove unconditional TLS certificate bypass in live test HTTP client
- Discard bytes while paused in _relayWithHostRewrite to prevent unbounded buffer growth
- Add 64 KB header size guard with socket teardown on overflow
- Add AGENTS.md, README.md, analysis_options.yaml to _tcp_proxy package

* fix: add lints dev_dependency to _tcp_proxy so analysis_options.yaml resolves

* fix: replace magic timing numbers with constants in integration tests

* docs: add DartDoc to _findCrlfCrlf explaining HTTP header boundary detection

* fix(signaling): reconnect silently on server force-close (code 4441) (#1012)

When the server sends disconnect code 4441 (controllerForceAttachClose)
it means two signaling sessions from the same account were open at the
same time — a race that can happen when a background push isolate is
still connected as the main engine comes to the foreground and
reconnects signaling.

Previously this code path set lastSignalingDisconnectCode, which
triggered CallStatus.connectIssue and showed a "Connection issue"
snackbar to the user even though the situation was transient and
self-healing.

Fix: on 4441, emit lastSignalingDisconnectCode=null (no connectIssue
UI), skip the notification, and reconnect with the fast 1s delay
instead of the default 3s. A warning log is emitted so the race remains
visible in logs for debugging.

* refactor: decouple AppLogger from LogzioLoggingService via RemoteLoggingService abstraction (#1011)

* refactor: decouple AppLogger from LogzioLoggingService via RemoteLoggingService abstraction

* refactor: move remoteMinLevel extraction to init to avoid cast in applyConfig

* refactor: add minLevel to RemoteLoggingService and remove cast from AppLogger

* refactor: reorder LogzioLoggingService fields to match RemoteLoggingService interface order

* refactor: remove labelsProvider from AppLogger state, pass labels explicitly

* refactor: rename regenerateRemoteLabels to updateRemoteLabels and dispose before reinitialize

* refactor: replace labels map with lazy callback in AppLogger to encapsulate labels retrieval

* fix: attach remote appender before applyConfig so early logs are forwarded to remote

* feat: enable and disable log anonymization with env (#1004)

* feat: enable and disable log anonymization with env

Added functionality to disable and enable log anonymization via environment variable

* refactor: simplify AnonymizationType to none/full enum with bool flag

Replace the mutable list-based anonymization approach with a single
AnonymizationType enum (none/full), removing the race condition risk
and simplifying the API surface.

* docs: document intentional anonymization scope in AppLogger

* fix: add @override annotation to setAnonymizationEnabled in LogzioLoggingService

---------

Co-authored-by: Dmytro Serdun <d.serdun@webtrit.com>

* fix: api healthcheck path (#1014)

* fix: use shared static logger in WebtritSignalingClient (#1013)

* fix: use shared static logger in WebtritSignalingClient with instance id prefix

* fix: restore custom logger support via inner constructor parameter

* fix: simplify logger to use fixed name without instance counter

* fix: remove custom logger parameter from inner constructor

* fix: signaling errors  (#1016)

* fix: snackbar duration

* fix: new persist option usage

* fix: reconnect message repeating

* fix: reconnect on keepalive timeout

* feat: user info cache (#1015)

* feat: main impl

* refactor: call to actions fix

* fix: call to actions provider

* docs: getAndListed explain

* fix: integration tests rescue (#1025)

* fix: migrate to 4.5

* fix: various fixes

* fix: remove log file from standart test

* fix: no audio if lone codec (#1028)

* fix: WebRTC signaling state guards — ICE restart, renegotiation, and glare (WT-986) (#1003)

* refactor: extract RenegotiationHandler with stable-state and concurrency guards

- Extract renegotiation logic from CallBloc into a standalone RenegotiationHandler class
- Add two stable-state guards: pre-offer check and TOCTOU guard after createOffer
- Add _isHandling/_pendingRetry flags to serialize concurrent onRenegotiationNeeded firings
- Catch WebtritSignalingErrorException for server-side error logging without swallowing
- Catch plain String errors (flutter_webrtc native) separately from Dart exceptions
- Add unit tests covering stable-state skip, concurrency serialization, and error paths
- Document server-mediated vs P2P topology constraints and Perfect Negotiation limitation

* fix: add signalingState guards and Perfect Negotiation rollback to call flow

ICE restart handler:
- Skip setLocalDescription when signalingState != stable to prevent native crash
- Log warning instead of silently skipping

Renegotiation / accepted handler:
- Guard setRemoteDescription(answer) against wrong state after glare resolution
- Log transceivers after setRemoteDescription for SDP debugging
- Catch String errors from setRemoteDescription to prevent unhandled exception escalation

Updating handler (Perfect Negotiation rollback):
- Pre-check signalingState for glare: if have-local-offer, roll back local offer before setRemoteDescription
- Catch String errors containing have-local-offer as fallback for stale flutter_webrtc signalingState cache
- Roll back and retry setRemoteDescription on confirmed glare

Renderer and state:
- Always refresh srcObject in RTCStreamView.didUpdateWidget to handle renegotiation-replaced tracks
- Fix remoteVideo getter to use logical OR (stream tracks || video flag) instead of short-circuit

Add call_state_test.dart covering ActiveCall equality and remoteVideo edge cases

* refactor: replace on String catch with typed RTC exceptions via RtcJsepErrorParser

* fix: guard RTCVideoRenderer srcObject assignment until initialize() completes (#1027)

* fix: guard RTCVideoRenderer srcObject assignment until initialize() completes

Prevents a crash where didUpdateWidget set srcObject before the renderer
was initialized, causing 'Call initialize before setting the stream'.

The _initialized flag ensures srcObject is only set after initialize()
resolves. Also always refreshes srcObject (not just on stream identity
change) to handle track replacement during renegotiation.

* fix: use setState and guard build before renderer initialization

Wrap _initialized and srcObject assignment in setState so the flag change
is properly synchronized with Flutter's build cycle. Guard build() to
avoid passing an uninitialized renderer to RTCVideoView — the
placeholderBuilder (or empty SizedBox) is shown until initialize()
completes.

* fix: update ExternalContactsSyncBloc tests to mock getAndListen instead of getLocalInfo (#1029)

* feat: handle 'voicemail_not_configured' error (#1005)

* feat: handle 'voicemail_not_configured' error

Added handling 'voicemail_not_configured' error from  voicemail api

* fix: properly handle voicemail_not_configured and endpoint_not_supported errors across all layers

- WebtritApiClient: skip SEVERE log for expected VoicemailNotConfiguredException / EndpointNotSupportedException
- VoicemailRepositoryImpl: add _featureSupported flag to skip API calls once feature is known unavailable; add _fetchingCompleter.future.ignore() to prevent unhandled future errors; suppress stack trace in warning log for expected exceptions; expose isFeatureSupported getter
- VoicemailRepository: add isFeatureSupported to abstract interface; EmptyVoicemailRepository returns false
- VoicemailCubit: check isFeatureSupported on init to immediately emit featureNotSupported without an API call
- SettingsBloc: catch expected exceptions from unawaited fetchVoicemails() to prevent runZonedGuarded propagation
- PollingService / ConnectivityLifecycleService: remove stack trace from WARNING logs in generic catch blocks

* fix: always allow navigation to voicemail screen regardless of feature support

Previously the settings tile was disabled with reduced opacity when voicemail
was not configured, which was confusing. Now the tile is always tappable and
the voicemail screen shows a placeholder explaining the reason.

* fix: move VoicemailCubit provider from SettingsScreenPage to VoicemailScreenPage

VoicemailCubit was provided at the settings level but consumed in a separate
route, causing ProviderNotFoundException on navigation. Moving it to
VoicemailScreenPage makes the provider scope match the consumer.

* fix: add missing call feature import to VoicemailScreenPage

* fix: remove unused stack trace variables from catch blocks in polling and connectivity services

* fix: address Copilot review comments

- VoicemailCubit: guard watchVoicemails subscription against overwriting featureNotSupported state
- VoicemailNotConfiguredException: pass token and error fields to super; remove commented-out placeholder
- WebtritApiClient: log xRequestId (never null) instead of requestId parameter; pass token/error to VoicemailNotConfiguredException
- SettingsTile: add assert that opacity is in [0.0, 1.0]

---------

Co-authored-by: Dmytro Serdun <d.serdun@webtrit.com>

* feat: add toJson() to all Event subclasses and fix NotifyEvent constructor inconsistency (#1030)

* fix: suppress transient network error SnackBar in RegisterStatusCubit (#1031)

Auto-triggered fetches (startup and connectivity restore) now use
_fetchStatusSilently(), which skips handleError for SocketException,
TimeoutException, and TlsException. The cubit retries automatically
on the next connectivity change, so surfacing these transient failures
to the user is misleading. User-initiated fetchStatus() retains the
original behaviour and still calls handleError for all errors.

* fix: add 10s timeout to reportNewIncomingCall in background message handler (WT-1061) (#1032)

Telecom on affected devices can become overloaded with phantom PhoneAccount
registrations, causing reportNewIncomingCall() to hang indefinitely. This
keeps FlutterFirebaseMessagingBackgroundService alive and blocks subsequent
cold starts. Adding a 10s timeout bounds the handler lifetime and logs a
warning when Telecom is slow to respond.

* fix: add 5s timeout to getInitialMessage() to prevent splash freeze (WT-1061) (#1033)

* refactor: remove tryUse from AppDatabaseScope, migrate callers to useOrNull (#1034)

* fix: eliminate write-write SQLite contention via shared DriftIsolate server (WT-1061) (#1035)

* fix: eliminate write-write SQLite contention via shared DriftIsolate server (WT-1061)

Spawns a single dedicated DriftIsolate server in the main isolate bootstrap
and registers its SendPort in IsolateNameServer under a fixed key.

Background isolates (FCM handler, WorkManager) now connect to the same server
via IsolateDatabase.connectOrCreate(), which looks up the port and creates a
client connection — falling back to a direct NativeDatabase connection when the
main app is not running (cold push with no foreground app).

All writes are serialized through the single server isolate, making
write-write SQLITE_BUSY (code=5) between concurrent isolates impossible.

Changes:
- app_database: add createAppDatabaseNative() for synchronous NativeDatabase
  creation inside the server isolate (no createInBackground needed)
- IsolateDatabase: add spawnServer(), connectOrCreate(), kDbPortName
- AppDatabaseScope.use(): connect via connectOrCreate() instead of create()
- bootstrap(): spawn DriftIsolate server, register DriftIsolate in InstanceRegistry
- AppDatabaseLifecycleHolder: connect to DriftIsolate, shutdown server on dispose

* fix: address Copilot review — robust spawnServer error handling and stale port cleanup (WT-1061)

* test: add integration tests for IsolateDatabase stale port handling (WT-1061)

* refactor: introduce SignalingModule stream abstraction (phase 1) (#1024)

* refactor: introduce SignalingModule stream abstraction (phase 1)

Replace SignalingManager callback-based API with SignalingModule — a
sealed-event broadcast stream that owns the WebtritSignalingClient
lifecycle without any BLoC, CallState, or UI dependency.

Key changes:
- Add SignalingModule with fire-and-forget connect(), disconnect(),
  dispose() and a sealed SignalingModuleEvent hierarchy
  (Connecting, Connected, ConnectionFailed, Disconnecting,
  Disconnected, HandshakeReceived, ProtocolEvent)
- Add isRepeated deduplication on ConnectionFailed to suppress
  repeated identical error notifications
- Map disconnect codes to recommendedReconnectDelay:
  4441 → Duration.zero, protocolError → null, all others → 3 s
- Migrate CallBloc from direct WebtritSignalingClient callbacks to
  SignalingModule stream subscription; new _SignalingClientEvent
  variants: connecting, connected, failed, disconnecting, disconnected
- Migrate IsolateManager (Push + Foreground) to SignalingModule,
  replacing SignalingManager; add connectivity monitoring and pending
  request queue inside IsolateManager
- Construct SignalingModule in main_shell.dart and inject into CallBloc
- Delete SignalingManager and remove its export from common.dart
- Add 31 unit and integration tests for SignalingModule

* fix(test): update ExternalContactsSyncBloc tests for getAndListen API

The BLoC was updated to call userRepository.getAndListen() instead of
getLocalInfo(), but the mocks were never updated. Fix the setUp mock
and correct the RefreshFailure test to use load() failure (which is
the actual trigger for that state) rather than userRepository failure.

* fix(test): rename local function to avoid leading underscore lint warning

* docs: translate signaling architecture doc to English

* docs: remove phase 1 requirements planning doc from repo

* refactor: remove coreUrl/tenantId/token/trustedCertificates from CallBloc

These four fields were passed through CallBloc only to construct
SignalingModule internally. Now that SignalingModule is constructed
externally and injected via the constructor, the fields are dead code.
Remove them and the corresponding import from ssl_certificates.

* fix: address Copilot review comments on SignalingModule/IsolateManager

- Guard delayed reconnect callbacks with signalingClient == null check to
  avoid tearing down a healthy connection that connected during the delay
- Populate _incomingCallEvents from handshake and protocol events so
  _findIncomingEventLog returns real caller data instead of null
- Use disconnect() instead of dispose() in handleLifecycleStatus so the
  module remains reusable when the app returns to the foreground
- Fix post-dispose connect() test to actually subscribe to the event stream
  and assert Connecting/Connected events are absent after dispose

* feat: replay session events to late subscribers in SignalingModule

Adds a per-subscriber replay buffer so that consumers created after
connect() (e.g. CallBloc constructed after SignalingModule already
connected and received a handshake) do not miss any events from the
current session.

- events getter now returns a single-subscription stream that first
  replays all events buffered since the last connect() call, then
  pipes live events from the broadcast controller
- connect() clears the buffer so late subscribers see only the
  current session, not stale events from previous reconnect cycles
- dispose() also clears the buffer on teardown
- Uses sync: true on the intermediate StreamController to avoid an
  extra async ho…
SERDUN added a commit that referenced this pull request May 22, 2026
* refactor: sync localizely keys and update plural logic (#921)

* refactor(app): implement safe teardown sequence for user logout (#922)

* refactor(app): implement safe teardown sequence for user logout

Introduces a dedicated `Teardown` application lifecycle state to ensure safe resource cleanup.

- Add `AppLifecycleStatus` to `AppBloc` to manage authentication states.
- Create `TeardownScreen` to hold the UI context while the `MainShell` unmounts.
- Implement `UserSessionCleanupResolver` to centralize repository and database clearing.
- Remove `onLogout` callback from `SessionRepository` and `main.dart`.
- Refactor `AppRouter` guards to handle the new teardown navigation flow.

* refactor: implement best-effort strategy for user session cleanup

* refactor: rename AppLogined to AppLoggedIn and refine logout documentation

* feat(autoprovision): pass system info when logging in via autoprovision

* refactor: implement memory-first session strategy and remove reactive streams

* fix: promote SessionRepository to a singleton in bootstrap

* refactor: implement intent-based logout orchestration in AppBloc

* style: optimize event instantiation and refine code comments

* style: reorder members in AppEvent classes

* feat: localize TeardownScreen progress text

* feat: explicitly handle storage cleanup during session updates

* refactor: refine teardown screen navigation guard

* docs: refine _saveSession documentation to match delegation pattern

* feat: Implement dynamic styling for LoginSwitch segmented button (#925)

* feat: add ButtonStyle and Geometry configuration models

* feat: implement configurable ButtonStyle for LoginSwitchScreen

* chore: add configurable SegmentedButton styling for login switch

* feat: add disabled state colors to ButtonStyleConfig

* feat: support disabled states and improve border side resolution in button styles

* refactor: migrate primary elevated button to ButtonStyleConfig

* refactor: convert CallPopupMenuButton to StatelessWidget and disable click effects (#923)

* refactor: replace custom tab buttons with standard TabBar and add unread badges (#924)

* refactor: replace custom tab buttons with standard TabBar and add unread badges

* refactor: remove redundant TabButtonsBar widget

* refactor: optimize tab selection logic and state updates

* fix(theme): propagate defaultFontFamily to theme factories (#926)

* fix(theme): propagate defaultFontFamily to theme factories

Updated theme style factories and configuration extensions to accept and apply
a `defaultFontFamily`. This ensures consistent font rendering across the app
by explicitly passing the font family derived from the global TextTheme into
custom component styles.

- Added `defaultFontFamily` parameter to `TextStyleConfig.toTextStyle` and
  related style conversion methods.
- Modified `ThemeStyleFactoryProvider` to initialize a `defaultTextTheme` and
  pass its font family to various screen and widget factories.
- Refactored multiple factories (Keypad, CallScreen, Login, Settings, etc.)
  to support the new font propagation logic.
- Replaced redundant `createTextTheme()` calls with a cached `defaultTextTheme`.

* refactor: improve initials text style mapping and reduce log verbosity

* fix(contacts): prevent race condition in LocalContactsSyncBloc during teardown (#927)

* refactor: re-engineer actionpad styling to semantic roles (#928)

* refactor: move ActionPadWidgetConfig from widget to page configuration

* refactor: add defaultVerticalAlignment to TextButtonsTable

* refactor(keypad): transition Actionpad to semantic button styling

Redesign Actionpad styling logic to move away from function-specific
identifiers toward semantic roles. This decoupling allows for more
flexible UI configurations and cleaner style inheritance.

* fix: correct button style merging and disabled states in ActionPad

* fix(call): increase monitor interval to optimize log quota (#929)

* refactor: remove hardcoded timeouts in PeerConnectionManager

* refactor: increase RtpTrafficMonitor check interval to 15s

Update the default monitorCheckInterval from 2s to 15s to optimize log
storage and prevent quota exhaustion. This adjustment reduces log
volume by approximately 85% while maintaining sufficient granularity
for monitoring network quality trends.

* feat: enhance RemoteConfigService with snapshots and stream support (#930)

* refactor: consolidate remote config service implementations

* refactor: enhance remote config service and caching logic

* refactor: move remote config service to services directory

* refactor: rename FirebaseRemoteConfigService to CachedRemoteConfigService

* feat: add remote config update stream support

* feat: integrate remote config overrides into FeatureAccess

* refactor: implement Snapshot pattern for RemoteConfigService

* refactor: update RemoteConfigService architecture and snapshot handling

* refactor: improve FeatureAccess reactivity and StreamUtils robustness

* refactor: add disposal guards to CachedRemoteConfigService and clean up redundant comments

* refactor: introduce FeatureAccessStreamFactory and decouple stream logic from RootApp

* refactor: update FeatureAccess architecture with specialized factories and rxdart integration

* refactor: ensure deterministic equality in CoreSupportImpl by sorting flags

* feat: dynamic RTP traffic monitoring interval and configuration synchronization (#931)

* feat: implement dynamic RTP monitoring configuration

- Added MonitoringConfig model and MonitoringMapper to handle monitor intervals.
- Updated PeerConnectionManager to support runtime configuration updates.
- Introduced CallConfigSynchronizer to reactively update CallBloc when FeatureAccess changes.
- Added 'monitorConfig' to SupportedFeature for static app configuration.
- Integrated Remote Config override for monitor check interval via 'feature_monitor_check_interval_sec'.
- Enabled voicemail settings by default in app.config.json.

* fix: align app.config.json key with SupportedMonitorConfig model

- Rename 'checkInterval' to 'checkIntervalSec' in app.config.json.
- Fixes an issue where the static monitor configuration was ignored due to a naming mismatch with the generated SupportedMonitorConfig model.
- Ensures the default 15-second interval is correctly read from the assets.

* fix(data): add monitoringConfig to FeatureAccess Equatable props

* fix(data): validate remote config monitor interval to prevent zero or negative values

* fix(call): allow disabling RTP monitor via zero interval

* test(data): verify monitor interval remote overrides and refactor validation

* docs: fix parameter name in SupportedFeature.monitorConfig doc comment

* fix: allow zero interval to propagate for RTP monitor disablement

* test(call): add unit tests for RtpTrafficMonitor

- Verified that checkInterval <= 0 successfully acts as a kill switch, preventing timer initialization and native calls.
- Verified that a valid positive interval correctly triggers periodic WebRTC stats fetching.
- Verified that monitoring automatically stops when RTCPeerConnection enters a closed state.
- Covered edge cases for zero and negative durations to ensure stability against invalid configurations.

* feat: generalize emergency reboot logic and refine diagnostic reporting (#932)

* feat: add reboot dialog

Added dialog for user to reboot user when timeout error on starting a call if it is Xiaomi phone

* refactor: replace AppMetadataProvider with DeviceInfo in DiagnosticService

* refactor: inject DeviceInfo into DiagnosticService in AppShell

* feat: include Huawei in emergency reboot diagnostic logic

* chore: update system error dialog localizations

* refactor(diagnostic): improve error handling and decouple UI logic

* refactor: simplify system error dialog and unify diagnostic launchers

* refactor: unify diagnostic flow and streamline reporting

* chore: remove trailing periods from system error dialog titles

---------

Co-authored-by: Alex Maudza <o.maudza@webtrit.com>

* refactor: add controller to HistoryAutocompleteField for manual history saving (#933)

* refactor: add controller to HistoryAutocompleteField for manual history saving

- Implement HistoryAutocompleteController to allow manual triggers for saving input to history.
- Convert LoginCoreUrlAssignScreen to a StatefulWidget to manage the controller lifecycle.
- Update HistoryAutocompleteField to support the new controller and internal logic extraction.
- Refactor callbacks into private methods and simplify widget building logic.

* fix: avoid redundant history saving in HistoryAutocompleteField

* feat: migrate system notifications and SIP presence to supported features (#934)

Refactor feature configuration by introducing systemNotifications and
sipPresence as variants of SupportedFeature. This change initiates
the transition away from legacy flat properties in AppConfigMain.

* fix: collect outbound rtp stats (#937)

* fix: description for Media encoding configs (#938)

Fixed description for Media encoding configs in Media settings. Full Flex option was removed, other options were renamed.

* fix(call): preserve widget context during auto-compact transition (#939)

Refactor CallActiveScaffold to use AnimatedOpacity and IgnorePointer
instead of conditional widget removal. This ensures that CallActions
remains in the widget tree, preventing callback failures (e.g.,
onAudioDeviceChanged) when the UI compacts while a popup is open.

* fix: fixed localization issues in network settings (#936)

Fixed issues with localization in network settings. Also added radio buttons and checkbox for 'Incoming Call Type' and 'SMS Fallback' accordingly

* chore: actualize l10n code generation (#940)

* chore: actualize configurations (#942)

* chore: implement modular rules system (#943)

* chore: implement modular rules system and copilot instructions (#946)

* chore: implement modular rules system and copilot instructions

* chore: add makefile helpers for copilot branch management

* feat: add theme switching settings (#951)

* chore: implement modular rules system and copilot instructions

* feat(settings): expose theme mode switching in settings

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>

* feat: remote cli settings (#944)

* feat: support metadata for missed call display name in isolate (#953)

* feat: support metadata for missed call display name in isolate

- Introduce getDisplayNameForMissedCall in IsolateManager to resolve caller names.
- Update PushNotificationIsolateManager to utilize CallkeepIncomingCallMetadata for missed call notifications.
- Pass metadata through onPushNotificationSyncCallback and onSignalingSyncCallback.
- Ensure LocalPushRepository returns the future from the notification plugin.

* docs: add documentation for IsolateManager properties

* refactor: improve missed call display name logic and logging

* feat(call): override _onNoActiveLines in PushNotificationIsolateManager for missed call handling (#959)

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: SERDUN <26858237+SERDUN@users.noreply.github.com>

* chore: ai agent pipeline refinement (#954)

* chore: ignore agent files

* docs: clarify commit message convention

* chore: enforce lowercase commit descriptions and update rules docs

* docs: refine AI agent execution pipeline and commit rules

* fix: user id migration (#960)

* refactor: replace SupportedMonitorConfig with SupportedLoggingConfig (#957)

* refactor: replace SupportedMonitorConfig with SupportedLoggingConfig

* refactor(logging): decouple AppLogger from RemoteConfigSnapshot

- make kLogLevelKey private in FeatureOverridesFactory
- add remoteLoggingEnabled to FeatureOverrides and LoggingConfig
- rewrite AppLogger.init() to accept LoggingConfig instead of RemoteConfigSnapshot
- add watchFeatureAccess() to observe loggingConfig changes via FeatureAccess stream
- add LoggingMapper.mapFromOverridesOnly() for isolate/background contexts
- update bootstrap.dart and services_isolate.dart call sites

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(logging): replace direct FeatureAccess stream with applyConfig in AppLogger

- remove watchFeatureAccess() and StreamSubscription from AppLogger
- add applyConfig(LoggingConfig) as the single public update point
- call applyConfig in App.didChangeDependencies() via existing FeatureAccess watch
- remove redundant feature_access.dart import from app_logger.dart

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(logging): narrow AppLogger.applyConfig to accept Level directly

- applyConfig now takes Level instead of LoggingConfig
- caller extracts loggingConfig.logLevel before passing to AppLogger

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(logging): remove LoggingConfig dependency from AppLogger

- init() now accepts Level and bool remoteLoggingEnabled as primitives
- remove models.dart import from app_logger.dart
- extract primitives from LoggingConfig at each call site

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(logging): remove EnvironmentConfig dependency from AppLogger

- init() now accepts logzioLogLevel and pre-built List<RemoteLoggingService>
- store logzioLogLevel as field for use in applyConfig
- move _buildRemoteLoggingServices to bootstrap.dart and services_isolate.dart
- remove environment_config.dart import from app_logger.dart

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(logging): simplify AppLogger by replacing List<RemoteLoggingService> with LogzioLoggingService?

- add LogzioLoggingService.fromEnvironment() factory to encapsulate EnvironmentConfig logic
- AppLogger.init() now accepts LogzioLoggingService? instead of List<RemoteLoggingService>
- remove _buildRemoteLoggingServices helpers from bootstrap.dart and services_isolate.dart

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(logging): move logzioLogLevel resolution into LogzioLoggingService.fromEnvironment

- fromEnvironment() now reads REMOTE_LOGZIO_LOG_LEVEL internally, no longer takes minLevel param
- AppLogger.init() drops logzioLogLevel parameter, uses _logzioService?.minLevel in applyConfig
- remove environment_config.dart import from services_isolate.dart

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(logging): add unit tests for LoggingMapper and FeatureAccessStreamFactory logging fields

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(call): compare monitorCheckInterval directly to avoid spurious CallBloc updates

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* chore: update Flutter to 3.41.2 and suppress experimental_member_use warnings (#962)

* chore: update Flutter to 3.41.2 and suppress experimental_member_use warnings

Both LockCachingAudioSource (just_audio) and TableMigration (drift) are
the only available APIs for their respective tasks with no stable
alternatives. Added ignore directives with explanatory comments.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: bump minimum Flutter SDK constraint to 3.41.2

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* chore: add claude code team settings (#965)

* chore: add Claude Code team settings and documentation

Add team-level `.claude/settings.json` with deny rules for keystores,
signing keys, and environment files. Add `.claude/settings.local.json`
to `.gitignore` and document settings levels in `docs/development.md`.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: add safe tool allow rules to team settings

Allow flutter test, analyze, dart format, dart fix, pub get, and
gen-l10n commands in team settings for all developers.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* fix: remove unused import in messaging_shell.dart (#966)

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* fix(settings): add optimistic toggle with spinner for register status switch (#967)

Replaces the blocking register status toggle with an optimistic update
pattern — the switch flips immediately, shows a spinner, disables during
the API call, and reverts on failure.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* fix: skip static payloads sanitizing if no rtpmap (#968)

* fix: media preset translation (#969)

* fix: fix splash config generation and add android_12 image support (#865)

- Escape # in Makefile color variables to prevent shell comment issues
- Remove redundant shell parameter expansion, use Make-only expansion
- Add ANDROID_12_SPLASH_IMAGE variable for separate Android 12 splash
- Use single quotes in echo to avoid shell interpretation problems

Co-authored-by: Dmytro Serdun <d.serdun@webtrit.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* chore: add diagnostic logging for recents number mismatch (#970)

Add hash-based and raw value logging to trace why recent calls list
shows two different phone numbers per entry. Raw values are visible
when Logz.io anonymization is disabled, hashes work with it enabled.

* feat: add support a blurred appbar surface (#971)

* feat: add BlurredSurface widget and apply blur effect to all app bars

Extract common ClipRect+BackdropFilter pattern into reusable BlurredSurface widget.
Apply consistent blur background with extendBodyBehindAppBar to all screens
using MainAppBar or AppBar. Remove isComplexBackground conditional logic.

* refactor: standardize MainAppBar field order across screens

* feat: apply blurred appbar surface to ConversationsScreen

* fix: remove unused theme/theme.dart imports

* fix: add top padding to prevent content overlap with blurred appbar

Screens using extendBodyBehindAppBar lacked compensating padding,
causing body content to be hidden behind the blurred AppBar. Added
appropriate top padding to recents, favorites, about, contacts, and
CDR screens consistent with the existing settings screen approach.

* feat(screenshots): make IgnorePointer configurable in ScreenshotApp (#972)

Add ignorePointer parameter (default true) to allow enabling interaction
for specific screenshots that require it.

* feat: add screenshot mocks and docs for all features (#973)

* feat: add screenshot mocks for all features

Add ~20 new screenshot definitions covering all user-visible screens:
- Core flows: contact, chat conversation, SMS conversation, system
  notifications, recent CDRs, number CDRs, call log
- Settings: network, language, diagnostic, caller ID, presence,
  theme mode, voicemail
- Login: switch screen
- Utility: log records console, contacts agreement, teardown,
  permissions, user agreement

Includes shared mock data, mock blocs/cubits/repositories,
screenshot widgets, router entries, and integration test registrations.

* docs: add screenshots package documentation

Add docs for screenshots architecture, mock reference, data reference,
and step-by-step guide for adding new screenshots. Update README with
links to the new documentation.

* fix: address review feedback for screenshot mocks and docs

- Add provider as direct dependency in screenshots/pubspec.yaml
  and remove ignore: depend_on_referenced_packages from 7 files
- Fix mock type/factory mismatches in mocks.md documentation
- Fix broken code formatting in mocks.md and adding_screenshots.md
- Remove shadowed ChatTypingCubit/SmsTypingCubit BlocProviders
  from conversation screenshot wrappers
- Guard registerFallbackValue with static flag to prevent
  duplicate registration
- Fix duplicated phrase in README.md Firebase Hosting section
- Document both flutter test and flutter drive workflows in
  overview.md

* feat: update keystores path to new applications subdirectory (#974)

* feat: add text style background decoration support (#975)

* feat: add backgroundBorderRadius and backgroundPadding to TextStyleConfig

* feat: add greetingTextStyle to LoginModeSelectPageConfig

* feat: add ExtendedText widget with background decoration support

* feat: add greetingTextStyle to login.modeSelect docs

* feat: add tests for ExtendedText widget

* feat: update theme template configs with missing components and fixes (#976)

- Fix calStatuses typo to callStatuses in both widget configs
- Add missing widget sections as examples: button, group, bar, input, text, confirmDialog
- Fix non-existent dark SVG asset references to use existing logos
- Remove unsupported fontFeatures from page text styles
- Clean up dark page config duplicate keys in login.switchPage
- Add greetingTextStyle for dark login modeSelect visibility
- Update dark widget decoration gradient config

* refactor: replace deprecated GradientColorsConfig with PageBackground (#977)

* refactor: replace deprecated GradientColorsConfig with PageBackground

Remove GradientColorsConfig, DecorationConfig, GradientsStyleFactory,
and Gradients theme extension. Migrate login and call screens to use
PageBackground via ThemedScaffold.

* refactor: enable gradient backgrounds for login and call page configs

Activate background for login modeSelect and add gradient background
to dialing section using colors from the removed GradientColorsConfig.

* refactor: adjust dark login gradient to contrast with buttons

Change bottom gradient color from #000000 to #0A1929 to avoid
blending with neutralOnDark button surface color.

* refactor: reverse dark login gradient direction

Start with dark color on top, lighter blue on bottom to keep
contrast with buttons at the bottom of the screen.

* refactor: rename misleading onSurface variable to surfaceColor

* feat: appBar blurred surface config and theme updates (#979)

* feat: extract appBar background color and blurred surface to config pipeline

Add configurable appBarBackgroundColor and appBarBlurredSurface (color, sigmaX, sigmaY)
to the theme config→style pipeline across all 7 main screens, replacing hardcoded values.

* feat: update page configuration docs with appBar fields

Add Common page fields section documenting appBarBackgroundColor
and appBarBlurredSurface, update Keypad page example.

* feat: remove appBarBackgroundColor from config pipeline

AppBar backgroundColor is already handled by MainAppBar fallback chain
(explicit → appBarTheme → canvasColor.withAlpha). Remove redundant
appBarBackgroundColor from BasePageConfig, all page configs, screen styles,
factories, and screens to avoid confusion.

* feat: add BlurredSurface.fromStyle factory and simplify screen usage

Replace inline BlurredSurface construction with fromStyle() factory
that returns null when no config is present (no blur applied) or a
configured BlurredSurface with resolved sigma defaults of 10.

* feat: enable appBar blur and activate widget config sections

- Add appBarBlurredSurface to all 7 page sections (dark/light)
- Activate previously ignored widget config sections (_bar, _button,
  _group, _input, _text -> remove underscore prefix)
- Set appBar backgroundColor/surfaceTintColor to transparent for blur
- Fix tab indicator label contrast and remove divider line

* feat: update light theme appBarBlurredSurface color to soft blue-white

* feat: set dark status bar icons for light theme appBar

* feat: fix appBarBlurredSurface sigma defaults and add tests

- Make sigmaX/sigmaY nullable in BlurredSurfaceConfig so omitted fields
  resolve to 10 via BlurredSurface.fromStyle instead of staying 0
- Regenerate Freezed/JSON for BlurredSurfaceConfig
- Restore blur on RecentCdrs and SystemNotifications screens by passing
  sigmaX: 10, sigmaY: 10 explicitly to const BlurredSurface()
- Explicit Colors.transparent fallback in BlurredSurface Container child
- Update docs to reflect null defaults with 10 resolved at widget layer
- Add BlurredSurface.fromStyle widget tests (4 cases)

* fix: system back button blocked in EmbeddedRequestErrorDialog (#935)

- Remove canPop: false from EmbeddedRequestErrorDialog — default is true,
  and blocking pop prevented the system back button from working in
  Settings → Terms & Conditions when the page failed to load (WT-890).
- Add onPopInvokedWithResult to call onBack when system back dismisses
  the dialog in Mode B (pushed route), ensuring _errorDialogShown is reset.
- Reset _errorDialogShown in _handleEmbeddedErrorState when error is
  cleared, so repeated errors after system-back dismissal are shown again.

Co-authored-by: Dmytro Serdun <d.serdun@webtrit.com>

* fix: prevent keepalive write-after-close and harden transaction lifecycle (#870)

* fix: prevent keepalive write-after-close and add regression test

* fix: clean up transaction on send failure in _executeTransaction

Move _addMessage inside the try-catch block so that if writing to a
closed socket throws WebtritSignalingBadStateException, the transaction
is properly removed from the _transactions map instead of leaking.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* test: add transaction cleanup and keepalive timer tests

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: add comment for closeCode guard in keepalive loop

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: address Copilot review comments on keepalive integration tests

- Fix compile error: replace called(greaterThan(0)) with called(1)
- Fix lint warning: await streamController.close() in tearDown
- Fix false-positive test: move closeCode mock before flushMicrotasks
  and add verify closeCode called(2) to catch timer-restart regression

---------

Co-authored-by: Dmytro Serdun <d.serdun@webtrit.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* fix: fixed app is locked in Bluetooth call profile after first call (#982)

* fix: fixed app is locked in Bluetooth call profile after first call

Fixed situation when user returns to YouTube or music playback, the audio remains degraded, like during a call. This happens only on Android.

* fix: improve comment for _onLastCallEnded to cover iOS and Android audio routing

---------

Co-authored-by: Dmytro Serdun <d.serdun@webtrit.com>

* feat: add melos v7 integration (#980)

* feat: add melos integration with codegen, test, format, and dependency scripts

* feat: fix melos scripts syntax for v7 and rename format to fmt

* feat: configure melos v7 with workspace and scripts in pubspec.yaml

- Add workspace: list to pubspec.yaml for Dart pub workspace / Melos v7 package discovery
- Add melos: section to pubspec.yaml with scripts for codegen, tests, formatting, dependencies
- Add resolution: workspace to all sub-package pubspec.yaml files
- Fix ssl_certificates publish_to typo (none; → 'none')
- Update screenshots SDK constraint to ^3.8.0
- Remove melos.yaml (ignored by Melos v7 — config lives in pubspec.yaml)

* feat: include root package in melos workspace via useRootAsPackage

* fix: replace non-ASCII em dashes with hyphens in pubspec.yaml comments

* feat: migrate makefile targets to melos scripts and mark deprecated

* fix: replace __ with _ in screenshots to fix unnecessary_underscores lint

* fix: rename melos script from run to start to avoid CLI conflict

The melos v7 CLI command `melos run <script>` conflicted with the script
named `run`. When running `melos run fmt`, melos treated `run` as the
script name and `fmt` as an argument, causing flutter to look for a file
named `fmt` instead of formatting code.

Renaming the script to `start` (and `run:ios` to `start:ios`) resolves
the ambiguity. Use `melos run start` to launch the app.

* feat: add melos smoke tests and update docs with melos commands

- Add tool/scripts/melos_smoke_test.sh for smoke testing safe scripts
- Add smoke:test melos script to pubspec.yaml
- Rename start to start:android for clarity
- Replace Makefile references with melos commands in docs/build.md
- Rewrite docs/make_file.md as full melos commands reference
- Update README.md to link to Melos Commands

* fix: address Copilot review comments

- Switch get/upgrade/outdated melos scripts from exec to run
  to correctly resolve from workspace root (not per-package)
- Align screenshots flutter constraint with workspace root (^3.41.2)
  to fix sdk/flutter constraint inconsistency (Dart 3.8 requires Flutter 3.41+)

* fix: reverse date divider in chat (#983)

Placed date divider before bunch of messages. Also changed displaying date in this divider. Added displaying options "Today", "Yesterday", day of week, like "Monday", and date in E, d MMM format, like "Tue, 6 Jan" , depending on comparison between date of message and today's date.

* feat: claude code setup (#981)

* feat: replace .rules with CLAUDE.md for Claude Code

* feat: configure .claude with hooks and allowed commands

* feat: add melos usage rules and update common commands

* feat: reduce CLAUDE.md duplication and soften melos usage rules

* feat: add webtrit_callkeep docs and package-level CLAUDE.md files

* feat: document call architecture, flows, isolates, and key patterns in CLAUDE.md

* feat: restructure AI agent memory files (AGENTS.md + CLAUDE.md split)

- Add root AGENTS.md (<100 lines): universal instructions for any AI tool
- Slim root CLAUDE.md (<50 lines): @imports + Claude-specific gotchas only
- Extract CallBloc architecture to docs/call_architecture.md
- Add AGENTS.md for all 10 packages (shared content, any agent)
- Remove package CLAUDE.md where content fully moved to AGENTS.md
- Keep package CLAUDE.md only where Claude-specific gotchas exist (data, ssl_certificates)
- Add CLAUDE.local.md to .gitignore

* feat: address Copilot review comments in PR #981

- Fix md_formatter.py docstring: was 'prettier', now correctly says 'markdownlint-cli2 --fix'
- Expand settings.json deny list to cover keystores (.jks, .keystore, .p12) and signing keys (.pem, .key, .p8)
- Fix AGENTS.md import groups: two '2.' entries corrected to sequential 1–6 numbering
- Fix packages/data/CLAUDE.md migration steps: misnumbered list corrected to 1–5
- Add working directory note to packages/data/AGENTS.md commands section
- Fix _web_socket_channel/AGENTS.md: 'pinned to' → 'constrained to' for caret constraint

* feat: remove webtrit_phone_keystores from deny list (dir is outside project scope)

* feat: configure gitignore, formatter, and analysis for generated files (#985)

* feat: configure gitignore, formatter, and analysis for generated files

- Add **/build/ and **/.claude/ to .gitignore
- Update lefthook pre-commit to skip *.g.dart / *.freezed.dart / *.gr.dart
- Replace melos fmt/fmt:check with find-based scripts that exclude generated files

* feat: update melos v7 integration across features, DAOs, and screenshots

* feat: allow feat/ as valid branch prefix alongside feature/ (#987)

* feat: allow feat/ as valid branch prefix alongside feature/

Update branch-name-check.sh and git-lint.yml to accept both feat/ and
feature/ prefixes. Add CONTRIBUTING.md documenting branch naming rules
and commit conventions.

* docs: fix table alignment in CONTRIBUTING.md

* docs: consolidate git conventions into CONTRIBUTING.md as single source of truth

- Remove .rules.md (referenced non-existent .rules/ directory)
- Update .github/copilot-instructions.md to reference CONTRIBUTING.md and AGENTS.md
- Add feat/ to branch pattern in copilot-instructions.md
- Fix pre-commit hook description in docs/development.md (dart format, not flutter analyze)
- Add CONTRIBUTING.md reference in docs/development.md
- Add docs/development.md pointer in CONTRIBUTING.md hooks section

* feat: favorites remote syncable (#984)

* refactor: cdr ui improvements, disconnect reason translations (#989)

* fix: use firstWhere instead of first in getAllContacts test to avoid ordering assumption (#991)

* fix: change contact_phones UNIQUE constraint to (number, label, contact_id) (#992)

* feat: change contact_phones UNIQUE constraint to (number, label, contact_id)

* feat: update ContactPhonesDao to use (number, label) pairs for stale-row deletion

* feat: persist per-label phone rows in ContactsLocalDataSource for DID-only accounts

* feat: add tests for DID-only contact phone handling and schema migration v20

* docs: add inline comment explaining grouping and merge logic in displayPhones

* refactor: rename lambda param p to phone in deleteOtherContactPhonesOfContactId call

* style: apply dart format to changed files

* docs: add DartDoc to deleteOtherContactPhonesOfContactId

* docs: add inline comments to deleteOtherContactPhonesOfContactId body

* style: replace non-ASCII arrow with hyphen in comment

* fix: use canonical label in displayPhones to prevent merged label from reaching favorites

* fix: recreate contact_phones via new table to preserve favorites FK reference

* test: add favorites FK integrity assertion for migration v20

* fix: resolve canonical ContactPhone by id before passing to favorites

* docs: document merged label display and canonical label favorites behavior

* refactor: replace ContactPhone display with flat fields in ContactPhoneDisplayEntry

- Replace synthetic ContactPhone display field with displayLabel (String)
  and displayFavorite (bool) - only the two fields that actually differ
  from the canonical phone
- Rename canonical field to phone for clarity
- Update displayPhones convenience getter to reconstruct ContactPhone
  from phone + flat display fields
- Update ContactPhoneTileAdapter to accept only primitives and closures,
  removing all model object dependencies
- Update ContactScreen loop to use displayPhoneEntries with closures
  capturing entry.phone directly
- Add widget tests for ContactPhoneTileAdapter covering all enable flags,
  callback wiring, transfer logic, and popup menu entries
- Add direct tests for displayPhoneEntries covering displayLabel,
  displayFavorite, and phone field contracts

* feat: melos exported env support (#993)

* fix: log records file stability improvements (#998)

* fix: guard File.delete() with exists() check in shareLogRecords

Prevents PathNotFoundException on Android 15 when the OS clears the
app cache directory under background memory pressure while the native
share sheet is open, causing the finally block to attempt deleting a
file that no longer exists (Crashlytics issue 25d22f4de9016c556b46cfacea29a20b).

* fix: delegate dispose Future in LogRecordsFileRepositoryImpl

Without awaiting or returning the Future from RotatingFileAppender.dispose(),
the file handle could remain unclosed after the caller awaited repository
disposal. Use direct return to delegate the Future without an async wrapper.

* fix: replace existsSync retry with async exists() in _getAllLogFilesWithRetry

existsSync() reads from the OS filesystem cache and can return false
immediately after forceFlush() even though the file is on disk.
Switching to async File.exists() forces a fresh stat() call that
bypasses the cache. Also reduces max wait from 10s (5x2s) to 1s (10x100ms).

* fix: guard forceFlush() with try/catch in readAllLogs

An I/O error during forceFlush() would propagate and abort readAllLogs
entirely, returning no logs to the caller. Catching the error allows
reading whatever files are already on disk.

* fix: guard forceFlush() with try/catch in cleanLogs

An I/O error during forceFlush() would abort cleanLogs entirely,
leaving stale log files on disk. Catching the error allows deletion
to proceed on whatever files are already present.

* fix: replace retry loop with async file discovery in log records

- Replace _getAllLogFilesWithRetry (10×100ms polling loop) with
  _getAllLogFilesAsync — single async pass using await file.exists()
  per rotation slot, bypassing OS filesystem cache without any delay
- Extend file discovery range to 0..keepRotateCount inclusive so
  rotated files (e.g. app_logs.log.1) are found even when base file
  is absent immediately after rotation
- Fix cleanLogs to use await _getAllLogFilesAsync() instead of
  synchronous getAllLogFiles() which relied on existsSync()
- Fix readAllLogs iteration order: remove files.reversed so newer
  file (rotation 0) is read before older rotated file (rotation 1)
- Remove redundant file.exists() guard inside readAllLogs loop since
  _getAllLogFilesAsync already guarantees file presence
- Add full test coverage: LogRecordsMemoryRepositoryImpl,
  ReadableRotatingFileAppender (readAllLogs + cleanLogs),
  LogRecordsFileRepositoryImpl — 26 tests total

* fix: update outdated comment in readAllLogs test

* feat: add Claude Code PostToolUse hooks (dart formatter + newline enforcer) (#995)

* fix: call dropped when user taps before app fully initializes (#997)

* fix: wait for signaling and registration before failing outgoing call

In __onCallPerformEventStarted, the early registration guard ran before
the signaling wait, causing calls to drop immediately when the user tapped
call before the socket initialized. Move the guard after the signaling wait
and extend the wait predicate to also require registration status to be
known (isHandshakeEstablished && isSignalingEstablished).

* fix: hold outgoing call as pending until routing state is available

When the user taps call before CallRoutingCubit has initialized (app just
launched, user info not yet fetched), wait for the first non-null routing
state instead of immediately failing. The call proceeds automatically once
routing state becomes available. If the cubit is disposed while waiting,
the call is silently dropped.

* fix: remove unused notification import from CallController

* test: add CallController.createCall unit tests

Cover immediate dispatch, pending wait, cubit disposal, and
CallUndefinedLineNotification scenarios.

* fix: remove unused optional params in _FakeCallRoutingState

* fix: remove unnecessary call_controller.dart import in test

* fix: address Copilot review — unawaited createCall and fast-fail on signaling failure

- Make createCall void by delegating to private _createCallAsync via unawaited, avoiding unawaited_futures lint at all call sites
- Add isFailure condition to signaling firstWhere predicate so outgoing calls fail immediately on signaling failure instead of waiting full timeout

* fix: update call_controller tests to use void createCall

Replace await controller.createCall(...) with call + await Future.delayed(Duration.zero)
to pump microtasks after createCall became void

* refactor: extract signaling wait predicate into named variables

* fix: use currentState instead of state for registration check after signaling wait

* fix: replace non-ASCII em dash with semicolon in comment

* refactor: remove redundant signalingConnected/registrationKnown vars, use CallState getters directly

* fix: await all addTrack calls before createOffer to prevent empty offer

* refactor: add TODO to provide CallController as singleton via RepositoryProvider

* fix: log warning when callRoutingCubit closes before routing state arrives

* refactor: add comment explaining callRoutingState await logic

* fix: add timeout to routing state wait and show NoInternetConnectionNotification on expiry

* refactor: extract _waitForRoutingState helper to clean up routing state await

* fix: catch unexpected errors from _createCallAsync and add timeout notification test

* fix: await reportNewIncomingCall in background FCM handler (#1001)

Without await the Pigeon IPC Future was fire-and-forget — the background
isolate was destroyed before the call reached the Kotlin side, so
PhoneConnectionService.startIncomingCall() was never invoked and the
incoming call UI never appeared.

* feat: provide CallController as singleton via RepositoryProvider in MainShell (#1000)

* feat: provide CallController as singleton via RepositoryProvider in MainShell

Register CallController once in MainShell widget tree after CallBloc,
CallRoutingCubit and NotificationsBloc are available. All seven call
sites now obtain the shared instance via late final field initializer
(context.read<CallController>()) instead of constructing a new object
per StatefulWidget.

* feat: replace RepositoryProvider with CallControllerScope InheritedWidget

RepositoryProvider is semantically for the data layer; CallController
is a UI-tier coordinator. Introduce CallControllerScope (InheritedWidget)
following the PresenceViewParams pattern already in the codebase.
All seven consumers now resolve the controller via CallControllerScope.of(context).

* fix: prevent stale CallController on MainShell rebuild

Store CallController in _MainShellState via ??= so it is created once
regardless of how many times build() reruns. Switch CallControllerScope.of
to getElementForInheritedWidgetOfExactType to avoid registering a rebuild
dependency that would never fire (updateShouldNotify is always false).

* feat: add doc comment for _callController field in _MainShellState

* fix: feature access mapper logic (#1007)

* fix: correct icon and text case assertions in integration tests (#1010)

* fix: correct icon and text case assertions in integration tests

* revert: remove accidentally committed logger from call_bloc.dart

* feat: add integration tests for webtrit_signaling keepalive and disconnect (#1009)

* feat: add integration tests for webtrit_signaling keepalive and disconnect

Add mock-based and live integration tests covering:
- Keepalive timeout (WebtritSignalingKeepaliveTransactionTimeoutException)
- Normal keepalive cycles (echo → timer restarts)
- Graceful disconnect (onDisconnect callback)
- Stream error (onError callback)
- Execute after disconnect (WebtritSignalingDisconnectedException)
- Request transaction timeout (non-keepalive variant)
- Live tests against a real server (skipped when credentials not set)
- Network simulation via pure-Dart WebSocket proxy with pause/resume
  to verify keepalive timeout fires on real packet drop

* feat: replace WebSocket proxy with raw TCP proxy in live signaling test

Drop packets at the TCP byte level instead of WebSocket frame level
for more realistic network simulation. The proxy rewrites the HTTP
Host header before forwarding so the server accepts the upgrade request.

* feat: extract TcpProxy into reusable _tcp_proxy package

Move the raw TCP proxy from the signaling test into a standalone
internal package packages/_tcp_proxy so it can be used as a
dev dependency in both webtrit_signaling tests and app-level tests.

* fix: address Copilot review comments on signaling tests and tcp proxy

- Fix expectLater() to pass Future directly instead of closure
- Make live test client nullable to prevent LateInitializationError in tearDown
- Use local signalingClient variable in each live test for clarity
- Replace localhost with 127.0.0.1 to avoid IPv6-first resolution issues
- Remove unconditional TLS certificate bypass in live test HTTP client
- Discard bytes while paused in _relayWithHostRewrite to prevent unbounded buffer growth
- Add 64 KB header size guard with socket teardown on overflow
- Add AGENTS.md, README.md, analysis_options.yaml to _tcp_proxy package

* fix: add lints dev_dependency to _tcp_proxy so analysis_options.yaml resolves

* fix: replace magic timing numbers with constants in integration tests

* docs: add DartDoc to _findCrlfCrlf explaining HTTP header boundary detection

* fix(signaling): reconnect silently on server force-close (code 4441) (#1012)

When the server sends disconnect code 4441 (controllerForceAttachClose)
it means two signaling sessions from the same account were open at the
same time — a race that can happen when a background push isolate is
still connected as the main engine comes to the foreground and
reconnects signaling.

Previously this code path set lastSignalingDisconnectCode, which
triggered CallStatus.connectIssue and showed a "Connection issue"
snackbar to the user even though the situation was transient and
self-healing.

Fix: on 4441, emit lastSignalingDisconnectCode=null (no connectIssue
UI), skip the notification, and reconnect with the fast 1s delay
instead of the default 3s. A warning log is emitted so the race remains
visible in logs for debugging.

* refactor: decouple AppLogger from LogzioLoggingService via RemoteLoggingService abstraction (#1011)

* refactor: decouple AppLogger from LogzioLoggingService via RemoteLoggingService abstraction

* refactor: move remoteMinLevel extraction to init to avoid cast in applyConfig

* refactor: add minLevel to RemoteLoggingService and remove cast from AppLogger

* refactor: reorder LogzioLoggingService fields to match RemoteLoggingService interface order

* refactor: remove labelsProvider from AppLogger state, pass labels explicitly

* refactor: rename regenerateRemoteLabels to updateRemoteLabels and dispose before reinitialize

* refactor: replace labels map with lazy callback in AppLogger to encapsulate labels retrieval

* fix: attach remote appender before applyConfig so early logs are forwarded to remote

* feat: enable and disable log anonymization with env (#1004)

* feat: enable and disable log anonymization with env

Added functionality to disable and enable log anonymization via environment variable

* refactor: simplify AnonymizationType to none/full enum with bool flag

Replace the mutable list-based anonymization approach with a single
AnonymizationType enum (none/full), removing the race condition risk
and simplifying the API surface.

* docs: document intentional anonymization scope in AppLogger

* fix: add @override annotation to setAnonymizationEnabled in LogzioLoggingService

---------

Co-authored-by: Dmytro Serdun <d.serdun@webtrit.com>

* fix: api healthcheck path (#1014)

* fix: use shared static logger in WebtritSignalingClient (#1013)

* fix: use shared static logger in WebtritSignalingClient with instance id prefix

* fix: restore custom logger support via inner constructor parameter

* fix: simplify logger to use fixed name without instance counter

* fix: remove custom logger parameter from inner constructor

* fix: signaling errors  (#1016)

* fix: snackbar duration

* fix: new persist option usage

* fix: reconnect message repeating

* fix: reconnect on keepalive timeout

* feat: user info cache (#1015)

* feat: main impl

* refactor: call to actions fix

* fix: call to actions provider

* docs: getAndListed explain

* fix: integration tests rescue (#1025)

* fix: migrate to 4.5

* fix: various fixes

* fix: remove log file from standart test

* fix: no audio if lone codec (#1028)

* fix: WebRTC signaling state guards — ICE restart, renegotiation, and glare (WT-986) (#1003)

* refactor: extract RenegotiationHandler with stable-state and concurrency guards

- Extract renegotiation logic from CallBloc into a standalone RenegotiationHandler class
- Add two stable-state guards: pre-offer check and TOCTOU guard after createOffer
- Add _isHandling/_pendingRetry flags to serialize concurrent onRenegotiationNeeded firings
- Catch WebtritSignalingErrorException for server-side error logging without swallowing
- Catch plain String errors (flutter_webrtc native) separately from Dart exceptions
- Add unit tests covering stable-state skip, concurrency serialization, and error paths
- Document server-mediated vs P2P topology constraints and Perfect Negotiation limitation

* fix: add signalingState guards and Perfect Negotiation rollback to call flow

ICE restart handler:
- Skip setLocalDescription when signalingState != stable to prevent native crash
- Log warning instead of silently skipping

Renegotiation / accepted handler:
- Guard setRemoteDescription(answer) against wrong state after glare resolution
- Log transceivers after setRemoteDescription for SDP debugging
- Catch String errors from setRemoteDescription to prevent unhandled exception escalation

Updating handler (Perfect Negotiation rollback):
- Pre-check signalingState for glare: if have-local-offer, roll back local offer before setRemoteDescription
- Catch String errors containing have-local-offer as fallback for stale flutter_webrtc signalingState cache
- Roll back and retry setRemoteDescription on confirmed glare

Renderer and state:
- Always refresh srcObject in RTCStreamView.didUpdateWidget to handle renegotiation-replaced tracks
- Fix remoteVideo getter to use logical OR (stream tracks || video flag) instead of short-circuit

Add call_state_test.dart covering ActiveCall equality and remoteVideo edge cases

* refactor: replace on String catch with typed RTC exceptions via RtcJsepErrorParser

* fix: guard RTCVideoRenderer srcObject assignment until initialize() completes (#1027)

* fix: guard RTCVideoRenderer srcObject assignment until initialize() completes

Prevents a crash where didUpdateWidget set srcObject before the renderer
was initialized, causing 'Call initialize before setting the stream'.

The _initialized flag ensures srcObject is only set after initialize()
resolves. Also always refreshes srcObject (not just on stream identity
change) to handle track replacement during renegotiation.

* fix: use setState and guard build before renderer initialization

Wrap _initialized and srcObject assignment in setState so the flag change
is properly synchronized with Flutter's build cycle. Guard build() to
avoid passing an uninitialized renderer to RTCVideoView — the
placeholderBuilder (or empty SizedBox) is shown until initialize()
completes.

* fix: update ExternalContactsSyncBloc tests to mock getAndListen instead of getLocalInfo (#1029)

* feat: handle 'voicemail_not_configured' error (#1005)

* feat: handle 'voicemail_not_configured' error

Added handling 'voicemail_not_configured' error from  voicemail api

* fix: properly handle voicemail_not_configured and endpoint_not_supported errors across all layers

- WebtritApiClient: skip SEVERE log for expected VoicemailNotConfiguredException / EndpointNotSupportedException
- VoicemailRepositoryImpl: add _featureSupported flag to skip API calls once feature is known unavailable; add _fetchingCompleter.future.ignore() to prevent unhandled future errors; suppress stack trace in warning log for expected exceptions; expose isFeatureSupported getter
- VoicemailRepository: add isFeatureSupported to abstract interface; EmptyVoicemailRepository returns false
- VoicemailCubit: check isFeatureSupported on init to immediately emit featureNotSupported without an API call
- SettingsBloc: catch expected exceptions from unawaited fetchVoicemails() to prevent runZonedGuarded propagation
- PollingService / ConnectivityLifecycleService: remove stack trace from WARNING logs in generic catch blocks

* fix: always allow navigation to voicemail screen regardless of feature support

Previously the settings tile was disabled with reduced opacity when voicemail
was not configured, which was confusing. Now the tile is always tappable and
the voicemail screen shows a placeholder explaining the reason.

* fix: move VoicemailCubit provider from SettingsScreenPage to VoicemailScreenPage

VoicemailCubit was provided at the settings level but consumed in a separate
route, causing ProviderNotFoundException on navigation. Moving it to
VoicemailScreenPage makes the provider scope match the consumer.

* fix: add missing call feature import to VoicemailScreenPage

* fix: remove unused stack trace variables from catch blocks in polling and connectivity services

* fix: address Copilot review comments

- VoicemailCubit: guard watchVoicemails subscription against overwriting featureNotSupported state
- VoicemailNotConfiguredException: pass token and error fields to super; remove commented-out placeholder
- WebtritApiClient: log xRequestId (never null) instead of requestId parameter; pass token/error to VoicemailNotConfiguredException
- SettingsTile: add assert that opacity is in [0.0, 1.0]

---------

Co-authored-by: Dmytro Serdun <d.serdun@webtrit.com>

* feat: add toJson() to all Event subclasses and fix NotifyEvent constructor inconsistency (#1030)

* fix: suppress transient network error SnackBar in RegisterStatusCubit (#1031)

Auto-triggered fetches (startup and connectivity restore) now use
_fetchStatusSilently(), which skips handleError for SocketException,
TimeoutException, and TlsException. The cubit retries automatically
on the next connectivity change, so surfacing these transient failures
to the user is misleading. User-initiated fetchStatus() retains the
original behaviour and still calls handleError for all errors.

* fix: add 10s timeout to reportNewIncomingCall in background message handler (WT-1061) (#1032)

Telecom on affected devices can become overloaded with phantom PhoneAccount
registrations, causing reportNewIncomingCall() to hang indefinitely. This
keeps FlutterFirebaseMessagingBackgroundService alive and blocks subsequent
cold starts. Adding a 10s timeout bounds the handler lifetime and logs a
warning when Telecom is slow to respond.

* fix: add 5s timeout to getInitialMessage() to prevent splash freeze (WT-1061) (#1033)

* refactor: remove tryUse from AppDatabaseScope, migrate callers to useOrNull (#1034)

* fix: eliminate write-write SQLite contention via shared DriftIsolate server (WT-1061) (#1035)

* fix: eliminate write-write SQLite contention via shared DriftIsolate server (WT-1061)

Spawns a single dedicated DriftIsolate server in the main isolate bootstrap
and registers its SendPort in IsolateNameServer under a fixed key.

Background isolates (FCM handler, WorkManager) now connect to the same server
via IsolateDatabase.connectOrCreate(), which looks up the port and creates a
client connection — falling back to a direct NativeDatabase connection when the
main app is not running (cold push with no foreground app).

All writes are serialized through the single server isolate, making
write-write SQLITE_BUSY (code=5) between concurrent isolates impossible.

Changes:
- app_database: add createAppDatabaseNative() for synchronous NativeDatabase
  creation inside the server isolate (no createInBackground needed)
- IsolateDatabase: add spawnServer(), connectOrCreate(), kDbPortName
- AppDatabaseScope.use(): connect via connectOrCreate() instead of create()
- bootstrap(): spawn DriftIsolate server, register DriftIsolate in InstanceRegistry
- AppDatabaseLifecycleHolder: connect to DriftIsolate, shutdown server on dispose

* fix: address Copilot review — robust spawnServer error handling and stale port cleanup (WT-1061)

* test: add integration tests for IsolateDatabase stale port handling (WT-1061)

* refactor: introduce SignalingModule stream abstraction (phase 1) (#1024)

* refactor: introduce SignalingModule stream abstraction (phase 1)

Replace SignalingManager callback-based API with SignalingModule — a
sealed-event broadcast stream that owns the WebtritSignalingClient
lifecycle without any BLoC, CallState, or UI dependency.

Key changes:
- Add SignalingModule with fire-and-forget connect(), disconnect(),
  dispose() and a sealed SignalingModuleEvent hierarchy
  (Connecting, Connected, ConnectionFailed, Disconnecting,
  Disconnected, HandshakeReceived, ProtocolEvent)
- Add isRepeated deduplication on ConnectionFailed to suppress
  repeated identical error notifications
- Map disconnect codes to recommendedReconnectDelay:
  4441 → Duration.zero, protocolError → null, all others → 3 s
- Migrate CallBloc from direct WebtritSignalingClient callbacks to
  SignalingModule stream subscription; new _SignalingClientEvent
  variants: connecting, connected, failed, disconnecting, disconnected
- Migrate IsolateManager (Push + Foreground) to SignalingModule,
  replacing SignalingManager; add connectivity monitoring and pending
  request queue inside IsolateManager
- Construct SignalingModule in main_shell.dart and inject into CallBloc
- Delete SignalingManager and remove its export from common.dart
- Add 31 unit and integration tests for SignalingModule

* fix(test): update ExternalContactsSyncBloc tests for getAndListen API

The BLoC was updated to call userRepository.getAndListen() instead of
getLocalInfo(), but the mocks were never updated. Fix the setUp mock
and correct the RefreshFailure test to use load() failure (which is
the actual trigger for that state) rather than userRepository failure.

* fix(test): rename local function to avoid leading underscore lint warning

* docs: translate signaling architecture doc to English

* docs: remove phase 1 requirements planning doc from repo

* refactor: remove coreUrl/tenantId/token/trustedCertificates from CallBloc

These four fields were passed through CallBloc only to construct
SignalingModule internally. Now that SignalingModule is constructed
externally and injected via the constructor, the fields are dead code.
Remove them and the corresponding import from ssl_certificates.

* fix: address Copilot review comments on SignalingModule/IsolateManager

- Guard delayed reconnect callbacks with signalingClient == null check to
  avoid tearing down a healthy connection that connected during the delay
- Populate _incomingCallEvents from handshake and protocol events so
  _findIncomingEventLog returns real caller data instead of null
- Use disconnect() instead of dispose() in handleLifecycleStatus so the
  module remains reusable when the app returns to the foreground
- Fix post-dispose connect() test to actually subscribe to the event stream
  and assert Connecting/Connected events are absent after dispose

* feat: replay session events to late subscribers in SignalingModule

Adds a per-subscriber replay buffer so that consumers created after
connect() (e.g. CallBloc constructed after SignalingModule already
connected and received a handshake) do not miss any events from the
current session.

- events getter now returns a single-subscription stream that first
  replays all events buffered since the last connect() call, then
  pipes live events from the broadcast controller
- connect() clears the buffer so late subscribers see only the
  current session, not stale events from previous reconnect cycles
- dispose() also clears the buffer on teardown
- Uses sync: true on the intermediate StreamController to avoid an
  extra async hop and keep delivery ordering consistent with callers
  that await module operations
- Adds two integration tests covering the late-subscriber replay
  and the buffer-clear-on-reconnect behaviours

* feat: connect SignalingModule early in initState to reduce call setup latency

SignalingModule is now created and connected in _MainShellState.initState(),
running the WebSocket handshake in parallel while the widget tree and
CallBloc are being built. When CallBloc is eventually created it subscribes
to the replay stream and receives all buffered session events without missing
anything.

_MainShellState.dispose() owns the module lifecycle; CallBloc.close()
still calls dispose() on the module (idempotent, safe).

* docs: update signaling architecture doc with layer descriptions and diagrams

* fix: add concurrency lock to _connectAsync to prevent parallel connects

* fix: await disconnect ack in dispose() to prevent SignalingDisconnected drop on race

* fix: suppress reconnect hint on intentional disconnect to prevent spurious reconnect

* fix: remove SignalingModule.dispose() from CallBloc.close() — ownership belongs to MainShellState

* fix: snapshot buffer before live subscribe to prevent replay duplicates in events getter

* fix: store reconnect Timer in IsolateManager so it can be cancelled on close()

* fix: forward recommendedReconnectDelay from SignalingDisconnected to _scheduleReconnect in CallBloc

* fix: suppress _onDisconnect after _onError to prevent double reconnect scheduling

* fix: exclude SignalingProtocolEvent from session buffer to prevent unbounded growth

* fix: replace force-unwrap of session.coreUrl/token with null-safe logout in initState

* fix: remove performEndCall early return so pre-handshake declines are queued

* fix: close liveController on subscription cancel to prevent StreamController leak

* fix: use _networkNone state instead of stale results snapshot in connectivity timer closure

* docs: fix _scheduleReconnectIfNeeded → _scheduleReconnect in signaling architecture doc

* test: replace Future.delayed(Duration.zero) with pumpEventQueue() in signaling module tests

* fix: make _controller sync:true to eliminate async-dispatch event duplication

* docs: clarify disconnect() docstring — SignalingDisconnected is callback-driven, not synchronous

* fix: wrap _signalingModule.disconnect() in unawaited() in _disconnectInitiated

* fix: remove unused shouldReconnect variable from __onSignalingClientEventDisconnected

* test: add 8 tests to reach 100% SignalingModule coverage

- concurrent connect() dropped while factory in-flight (_connecting guard)
- intentional disconnect() emits SignalingDisconnected with null delay
- disconnect() passes goingAway code to the underlying client
- _onError suppresses subsequent _onDisconnect (_errorHandled flag)
- SignalingProtocolEvent excluded from replay buffer
- cancelled subscription receives no further events
- dispose() awaits disconnect ack before closing the stream
- _onHandshake/_onEvent are no-ops after dispose()

* test: add scenario-driven SignalingModule tests from CallBloc usage analysis

Covers scenarios observed in CallBloc's signaling subscription:

internet dropped mid-session:
- _onError after handshake → ConnectionFailed not Disconnected, signalingClient cleared
- Unexpected socket close (null code) → Disconnected with kSignalingClientReconnectDelay
- ConnectionFailed buffered so late subscribers reconstruct last-known failure

handshake not completed:
- Disconnect before handshake → no HandshakeReceived in buffer, Disconnected with delay
- Late subscriber after no-handshake disconnect → gets Connecting+Connected+Disconnected only
- Error before handshake → ConnectionFailed buffered, no HandshakeReceived
- Reconnect after no-handshake failure delivers fresh session events

late subscriber mid-session:
- Factory still pending → gets Connecting from buffer, Connected arrives live
- After full connect+handshake → all three lifecycle events replayed, no protocol events

disconnect() robustness:
- client.disconnect() throws → dispose() still completes without hanging
- Second disconnect() with no active client is a silent no-op

* fix: restore callkeep_signaling_status_converter.dart lost during rebase

* fix: remove unused fields in _ThrowingDisconnectClient test helper

* fix: use normalClosure (1000) instead of goingAway (1001) when client disconnects WebSocket

* test: update disconnect test to expect normalClosure (1000) instead of goingAway (1001)

* fix: address Copilot review comments in SignalingModule and IsolateManager

- Fix doc comment on events getter: protocol events are not replayed, only lifecycle/handshake events
- Guard connect() buffer clear behind _connecting check to avoid clearing on redundant calls
- Remove stale comment in _onDisconnect that contradicted the !_disposed guard
- Treat empty connectivity result as offline in _monitorConnectivity (results.isEmpty || any(none))
- Treat empty connectivity result as offline in performAnswerCall (isNotEmpty && !contains(none))

* fix: address post-review issues in SignalingModule and IsolateManager

- isolate_manager: fix connectivityNoneCounter reset — error now fires
  exactly once at maxConnectivityNoneRepeats; subsequent none-events are
  silently ignored until connectivity is restored and counter resets to 0

- main_shell: split SignalingModule construction into valid/invalid-creds
  branches to remove ?? '' fallbacks and make intent explicit

- signaling_module: document sync:true reentrancy assumption, single-use
  constraint on events getter, and _errorHandled ordering invariant

* revert: restore main_shell.dart SignalingModule construction with ?? '' fallback

The split-branch approach still created a module with empty strings in the
null-creds case — identical in behaviour to the original. Reverted to the
original form which is honest about the fallback until a proper nullable
refactor is done.

* fix: remove dead null-guard in MainShellState.initState for SignalingModule

The router guard (onMainShellRouteGuardNavigation) redirects to login
when state.status != authenticated, so coreUrl and token are always
non-null when MainShell is mounted. Replace ?? '' fallbacks and the
unreachable null-branch with direct ! unwraps.

* docs: sync signaling_architecture_target.md and call_architecture.md with current code

- signaling_architecture_target: add timer cancellation to IsolateManager
  and CallBloc _scheduleReconnect snippets (Future.delayed → Timer with cancel)
- signaling_architecture_target: add missing SignalingDisconnecting case to
  CallBloc subscription snippet
- call_architecture: update ownership — CallBloc owns SignalingModule, not
  WebtritSignalingClient directly

* fix: replace non-ASCII characters with ASCII equivalents in Dart sources

Replace em dash (U+2014) with '-' and right arrow (U+2192) with '->'
in comments and test descriptions across signaling_module.dart,
isolate_manager.dart, signaling_module_test.dart, and call_bloc.dart.

* fix: incorrect styling of status bar on app start (#1006)

Fixed status bar rendering with incorrect styling on initial app launch when theme is light

* feat: show pr…
SERDUN added a commit that referenced this pull request May 22, 2026
* feat: generalize emergency reboot logic and refine diagnostic reporting (#932)

* feat: add reboot dialog

Added dialog for user to reboot user when timeout error on starting a call if it is Xiaomi phone

* refactor: replace AppMetadataProvider with DeviceInfo in DiagnosticService

* refactor: inject DeviceInfo into DiagnosticService in AppShell

* feat: include Huawei in emergency reboot diagnostic logic

* chore: update system error dialog localizations

* refactor(diagnostic): improve error handling and decouple UI logic

* refactor: simplify system error dialog and unify diagnostic launchers

* refactor: unify diagnostic flow and streamline reporting

* chore: remove trailing periods from system error dialog titles

---------

Co-authored-by: Alex Maudza <o.maudza@webtrit.com>

* refactor: add controller to HistoryAutocompleteField for manual history saving (#933)

* refactor: add controller to HistoryAutocompleteField for manual history saving

- Implement HistoryAutocompleteController to allow manual triggers for saving input to history.
- Convert LoginCoreUrlAssignScreen to a StatefulWidget to manage the controller lifecycle.
- Update HistoryAutocompleteField to support the new controller and internal logic extraction.
- Refactor callbacks into private methods and simplify widget building logic.

* fix: avoid redundant history saving in HistoryAutocompleteField

* feat: migrate system notifications and SIP presence to supported features (#934)

Refactor feature configuration by introducing systemNotifications and
sipPresence as variants of SupportedFeature. This change initiates
the transition away from legacy flat properties in AppConfigMain.

* fix: collect outbound rtp stats (#937)

* fix: description for Media encoding configs (#938)

Fixed description for Media encoding configs in Media settings. Full Flex option was removed, other options were renamed.

* fix(call): preserve widget context during auto-compact transition (#939)

Refactor CallActiveScaffold to use AnimatedOpacity and IgnorePointer
instead of conditional widget removal. This ensures that CallActions
remains in the widget tree, preventing callback failures (e.g.,
onAudioDeviceChanged) when the UI compacts while a popup is open.

* fix: fixed localization issues in network settings (#936)

Fixed issues with localization in network settings. Also added radio buttons and checkbox for 'Incoming Call Type' and 'SMS Fallback' accordingly

* chore: actualize l10n code generation (#940)

* chore: actualize configurations (#942)

* chore: implement modular rules system (#943)

* chore: implement modular rules system and copilot instructions (#946)

* chore: implement modular rules system and copilot instructions

* chore: add makefile helpers for copilot branch management

* feat: add theme switching settings (#951)

* chore: implement modular rules system and copilot instructions

* feat(settings): expose theme mode switching in settings

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>

* feat: remote cli settings (#944)

* feat: support metadata for missed call display name in isolate (#953)

* feat: support metadata for missed call display name in isolate

- Introduce getDisplayNameForMissedCall in IsolateManager to resolve caller names.
- Update PushNotificationIsolateManager to utilize CallkeepIncomingCallMetadata for missed call notifications.
- Pass metadata through onPushNotificationSyncCallback and onSignalingSyncCallback.
- Ensure LocalPushRepository returns the future from the notification plugin.

* docs: add documentation for IsolateManager properties

* refactor: improve missed call display name logic and logging

* feat(call): override _onNoActiveLines in PushNotificationIsolateManager for missed call handling (#959)

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: SERDUN <26858237+SERDUN@users.noreply.github.com>

* chore: ai agent pipeline refinement (#954)

* chore: ignore agent files

* docs: clarify commit message convention

* chore: enforce lowercase commit descriptions and update rules docs

* docs: refine AI agent execution pipeline and commit rules

* fix: user id migration (#960)

* refactor: replace SupportedMonitorConfig with SupportedLoggingConfig (#957)

* refactor: replace SupportedMonitorConfig with SupportedLoggingConfig

* refactor(logging): decouple AppLogger from RemoteConfigSnapshot

- make kLogLevelKey private in FeatureOverridesFactory
- add remoteLoggingEnabled to FeatureOverrides and LoggingConfig
- rewrite AppLogger.init() to accept LoggingConfig instead of RemoteConfigSnapshot
- add watchFeatureAccess() to observe loggingConfig changes via FeatureAccess stream
- add LoggingMapper.mapFromOverridesOnly() for isolate/background contexts
- update bootstrap.dart and services_isolate.dart call sites

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(logging): replace direct FeatureAccess stream with applyConfig in AppLogger

- remove watchFeatureAccess() and StreamSubscription from AppLogger
- add applyConfig(LoggingConfig) as the single public update point
- call applyConfig in App.didChangeDependencies() via existing FeatureAccess watch
- remove redundant feature_access.dart import from app_logger.dart

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(logging): narrow AppLogger.applyConfig to accept Level directly

- applyConfig now takes Level instead of LoggingConfig
- caller extracts loggingConfig.logLevel before passing to AppLogger

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(logging): remove LoggingConfig dependency from AppLogger

- init() now accepts Level and bool remoteLoggingEnabled as primitives
- remove models.dart import from app_logger.dart
- extract primitives from LoggingConfig at each call site

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(logging): remove EnvironmentConfig dependency from AppLogger

- init() now accepts logzioLogLevel and pre-built List<RemoteLoggingService>
- store logzioLogLevel as field for use in applyConfig
- move _buildRemoteLoggingServices to bootstrap.dart and services_isolate.dart
- remove environment_config.dart import from app_logger.dart

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(logging): simplify AppLogger by replacing List<RemoteLoggingService> with LogzioLoggingService?

- add LogzioLoggingService.fromEnvironment() factory to encapsulate EnvironmentConfig logic
- AppLogger.init() now accepts LogzioLoggingService? instead of List<RemoteLoggingService>
- remove _buildRemoteLoggingServices helpers from bootstrap.dart and services_isolate.dart

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(logging): move logzioLogLevel resolution into LogzioLoggingService.fromEnvironment

- fromEnvironment() now reads REMOTE_LOGZIO_LOG_LEVEL internally, no longer takes minLevel param
- AppLogger.init() drops logzioLogLevel parameter, uses _logzioService?.minLevel in applyConfig
- remove environment_config.dart import from services_isolate.dart

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(logging): add unit tests for LoggingMapper and FeatureAccessStreamFactory logging fields

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(call): compare monitorCheckInterval directly to avoid spurious CallBloc updates

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* chore: update Flutter to 3.41.2 and suppress experimental_member_use warnings (#962)

* chore: update Flutter to 3.41.2 and suppress experimental_member_use warnings

Both LockCachingAudioSource (just_audio) and TableMigration (drift) are
the only available APIs for their respective tasks with no stable
alternatives. Added ignore directives with explanatory comments.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: bump minimum Flutter SDK constraint to 3.41.2

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* chore: add claude code team settings (#965)

* chore: add Claude Code team settings and documentation

Add team-level `.claude/settings.json` with deny rules for keystores,
signing keys, and environment files. Add `.claude/settings.local.json`
to `.gitignore` and document settings levels in `docs/development.md`.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: add safe tool allow rules to team settings

Allow flutter test, analyze, dart format, dart fix, pub get, and
gen-l10n commands in team settings for all developers.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* fix: remove unused import in messaging_shell.dart (#966)

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* fix(settings): add optimistic toggle with spinner for register status switch (#967)

Replaces the blocking register status toggle with an optimistic update
pattern — the switch flips immediately, shows a spinner, disables during
the API call, and reverts on failure.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* fix: skip static payloads sanitizing if no rtpmap (#968)

* fix: media preset translation (#969)

* fix: fix splash config generation and add android_12 image support (#865)

- Escape # in Makefile color variables to prevent shell comment issues
- Remove redundant shell parameter expansion, use Make-only expansion
- Add ANDROID_12_SPLASH_IMAGE variable for separate Android 12 splash
- Use single quotes in echo to avoid shell interpretation problems

Co-authored-by: Dmytro Serdun <d.serdun@webtrit.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* chore: add diagnostic logging for recents number mismatch (#970)

Add hash-based and raw value logging to trace why recent calls list
shows two different phone numbers per entry. Raw values are visible
when Logz.io anonymization is disabled, hashes work with it enabled.

* feat: add support a blurred appbar surface (#971)

* feat: add BlurredSurface widget and apply blur effect to all app bars

Extract common ClipRect+BackdropFilter pattern into reusable BlurredSurface widget.
Apply consistent blur background with extendBodyBehindAppBar to all screens
using MainAppBar or AppBar. Remove isComplexBackground conditional logic.

* refactor: standardize MainAppBar field order across screens

* feat: apply blurred appbar surface to ConversationsScreen

* fix: remove unused theme/theme.dart imports

* fix: add top padding to prevent content overlap with blurred appbar

Screens using extendBodyBehindAppBar lacked compensating padding,
causing body content to be hidden behind the blurred AppBar. Added
appropriate top padding to recents, favorites, about, contacts, and
CDR screens consistent with the existing settings screen approach.

* feat(screenshots): make IgnorePointer configurable in ScreenshotApp (#972)

Add ignorePointer parameter (default true) to allow enabling interaction
for specific screenshots that require it.

* feat: add screenshot mocks and docs for all features (#973)

* feat: add screenshot mocks for all features

Add ~20 new screenshot definitions covering all user-visible screens:
- Core flows: contact, chat conversation, SMS conversation, system
  notifications, recent CDRs, number CDRs, call log
- Settings: network, language, diagnostic, caller ID, presence,
  theme mode, voicemail
- Login: switch screen
- Utility: log records console, contacts agreement, teardown,
  permissions, user agreement

Includes shared mock data, mock blocs/cubits/repositories,
screenshot widgets, router entries, and integration test registrations.

* docs: add screenshots package documentation

Add docs for screenshots architecture, mock reference, data reference,
and step-by-step guide for adding new screenshots. Update README with
links to the new documentation.

* fix: address review feedback for screenshot mocks and docs

- Add provider as direct dependency in screenshots/pubspec.yaml
  and remove ignore: depend_on_referenced_packages from 7 files
- Fix mock type/factory mismatches in mocks.md documentation
- Fix broken code formatting in mocks.md and adding_screenshots.md
- Remove shadowed ChatTypingCubit/SmsTypingCubit BlocProviders
  from conversation screenshot wrappers
- Guard registerFallbackValue with static flag to prevent
  duplicate registration
- Fix duplicated phrase in README.md Firebase Hosting section
- Document both flutter test and flutter drive workflows in
  overview.md

* feat: update keystores path to new applications subdirectory (#974)

* feat: add text style background decoration support (#975)

* feat: add backgroundBorderRadius and backgroundPadding to TextStyleConfig

* feat: add greetingTextStyle to LoginModeSelectPageConfig

* feat: add ExtendedText widget with background decoration support

* feat: add greetingTextStyle to login.modeSelect docs

* feat: add tests for ExtendedText widget

* feat: update theme template configs with missing components and fixes (#976)

- Fix calStatuses typo to callStatuses in both widget configs
- Add missing widget sections as examples: button, group, bar, input, text, confirmDialog
- Fix non-existent dark SVG asset references to use existing logos
- Remove unsupported fontFeatures from page text styles
- Clean up dark page config duplicate keys in login.switchPage
- Add greetingTextStyle for dark login modeSelect visibility
- Update dark widget decoration gradient config

* refactor: replace deprecated GradientColorsConfig with PageBackground (#977)

* refactor: replace deprecated GradientColorsConfig with PageBackground

Remove GradientColorsConfig, DecorationConfig, GradientsStyleFactory,
and Gradients theme extension. Migrate login and call screens to use
PageBackground via ThemedScaffold.

* refactor: enable gradient backgrounds for login and call page configs

Activate background for login modeSelect and add gradient background
to dialing section using colors from the removed GradientColorsConfig.

* refactor: adjust dark login gradient to contrast with buttons

Change bottom gradient color from #000000 to #0A1929 to avoid
blending with neutralOnDark button surface color.

* refactor: reverse dark login gradient direction

Start with dark color on top, lighter blue on bottom to keep
contrast with buttons at the bottom of the screen.

* refactor: rename misleading onSurface variable to surfaceColor

* feat: appBar blurred surface config and theme updates (#979)

* feat: extract appBar background color and blurred surface to config pipeline

Add configurable appBarBackgroundColor and appBarBlurredSurface (color, sigmaX, sigmaY)
to the theme config→style pipeline across all 7 main screens, replacing hardcoded values.

* feat: update page configuration docs with appBar fields

Add Common page fields section documenting appBarBackgroundColor
and appBarBlurredSurface, update Keypad page example.

* feat: remove appBarBackgroundColor from config pipeline

AppBar backgroundColor is already handled by MainAppBar fallback chain
(explicit → appBarTheme → canvasColor.withAlpha). Remove redundant
appBarBackgroundColor from BasePageConfig, all page configs, screen styles,
factories, and screens to avoid confusion.

* feat: add BlurredSurface.fromStyle factory and simplify screen usage

Replace inline BlurredSurface construction with fromStyle() factory
that returns null when no config is present (no blur applied) or a
configured BlurredSurface with resolved sigma defaults of 10.

* feat: enable appBar blur and activate widget config sections

- Add appBarBlurredSurface to all 7 page sections (dark/light)
- Activate previously ignored widget config sections (_bar, _button,
  _group, _input, _text -> remove underscore prefix)
- Set appBar backgroundColor/surfaceTintColor to transparent for blur
- Fix tab indicator label contrast and remove divider line

* feat: update light theme appBarBlurredSurface color to soft blue-white

* feat: set dark status bar icons for light theme appBar

* feat: fix appBarBlurredSurface sigma defaults and add tests

- Make sigmaX/sigmaY nullable in BlurredSurfaceConfig so omitted fields
  resolve to 10 via BlurredSurface.fromStyle instead of staying 0
- Regenerate Freezed/JSON for BlurredSurfaceConfig
- Restore blur on RecentCdrs and SystemNotifications screens by passing
  sigmaX: 10, sigmaY: 10 explicitly to const BlurredSurface()
- Explicit Colors.transparent fallback in BlurredSurface Container child
- Update docs to reflect null defaults with 10 resolved at widget layer
- Add BlurredSurface.fromStyle widget tests (4 cases)

* fix: system back button blocked in EmbeddedRequestErrorDialog (#935)

- Remove canPop: false from EmbeddedRequestErrorDialog — default is true,
  and blocking pop prevented the system back button from working in
  Settings → Terms & Conditions when the page failed to load (WT-890).
- Add onPopInvokedWithResult to call onBack when system back dismisses
  the dialog in Mode B (pushed route), ensuring _errorDialogShown is reset.
- Reset _errorDialogShown in _handleEmbeddedErrorState when error is
  cleared, so repeated errors after system-back dismissal are shown again.

Co-authored-by: Dmytro Serdun <d.serdun@webtrit.com>

* fix: prevent keepalive write-after-close and harden transaction lifecycle (#870)

* fix: prevent keepalive write-after-close and add regression test

* fix: clean up transaction on send failure in _executeTransaction

Move _addMessage inside the try-catch block so that if writing to a
closed socket throws WebtritSignalingBadStateException, the transaction
is properly removed from the _transactions map instead of leaking.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* test: add transaction cleanup and keepalive timer tests

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: add comment for closeCode guard in keepalive loop

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: address Copilot review comments on keepalive integration tests

- Fix compile error: replace called(greaterThan(0)) with called(1)
- Fix lint warning: await streamController.close() in tearDown
- Fix false-positive test: move closeCode mock before flushMicrotasks
  and add verify closeCode called(2) to catch timer-restart regression

---------

Co-authored-by: Dmytro Serdun <d.serdun@webtrit.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* fix: fixed app is locked in Bluetooth call profile after first call (#982)

* fix: fixed app is locked in Bluetooth call profile after first call

Fixed situation when user returns to YouTube or music playback, the audio remains degraded, like during a call. This happens only on Android.

* fix: improve comment for _onLastCallEnded to cover iOS and Android audio routing

---------

Co-authored-by: Dmytro Serdun <d.serdun@webtrit.com>

* feat: add melos v7 integration (#980)

* feat: add melos integration with codegen, test, format, and dependency scripts

* feat: fix melos scripts syntax for v7 and rename format to fmt

* feat: configure melos v7 with workspace and scripts in pubspec.yaml

- Add workspace: list to pubspec.yaml for Dart pub workspace / Melos v7 package discovery
- Add melos: section to pubspec.yaml with scripts for codegen, tests, formatting, dependencies
- Add resolution: workspace to all sub-package pubspec.yaml files
- Fix ssl_certificates publish_to typo (none; → 'none')
- Update screenshots SDK constraint to ^3.8.0
- Remove melos.yaml (ignored by Melos v7 — config lives in pubspec.yaml)

* feat: include root package in melos workspace via useRootAsPackage

* fix: replace non-ASCII em dashes with hyphens in pubspec.yaml comments

* feat: migrate makefile targets to melos scripts and mark deprecated

* fix: replace __ with _ in screenshots to fix unnecessary_underscores lint

* fix: rename melos script from run to start to avoid CLI conflict

The melos v7 CLI command `melos run <script>` conflicted with the script
named `run`. When running `melos run fmt`, melos treated `run` as the
script name and `fmt` as an argument, causing flutter to look for a file
named `fmt` instead of formatting code.

Renaming the script to `start` (and `run:ios` to `start:ios`) resolves
the ambiguity. Use `melos run start` to launch the app.

* feat: add melos smoke tests and update docs with melos commands

- Add tool/scripts/melos_smoke_test.sh for smoke testing safe scripts
- Add smoke:test melos script to pubspec.yaml
- Rename start to start:android for clarity
- Replace Makefile references with melos commands in docs/build.md
- Rewrite docs/make_file.md as full melos commands reference
- Update README.md to link to Melos Commands

* fix: address Copilot review comments

- Switch get/upgrade/outdated melos scripts from exec to run
  to correctly resolve from workspace root (not per-package)
- Align screenshots flutter constraint with workspace root (^3.41.2)
  to fix sdk/flutter constraint inconsistency (Dart 3.8 requires Flutter 3.41+)

* fix: reverse date divider in chat (#983)

Placed date divider before bunch of messages. Also changed displaying date in this divider. Added displaying options "Today", "Yesterday", day of week, like "Monday", and date in E, d MMM format, like "Tue, 6 Jan" , depending on comparison between date of message and today's date.

* feat: claude code setup (#981)

* feat: replace .rules with CLAUDE.md for Claude Code

* feat: configure .claude with hooks and allowed commands

* feat: add melos usage rules and update common commands

* feat: reduce CLAUDE.md duplication and soften melos usage rules

* feat: add webtrit_callkeep docs and package-level CLAUDE.md files

* feat: document call architecture, flows, isolates, and key patterns in CLAUDE.md

* feat: restructure AI agent memory files (AGENTS.md + CLAUDE.md split)

- Add root AGENTS.md (<100 lines): universal instructions for any AI tool
- Slim root CLAUDE.md (<50 lines): @imports + Claude-specific gotchas only
- Extract CallBloc architecture to docs/call_architecture.md
- Add AGENTS.md for all 10 packages (shared content, any agent)
- Remove package CLAUDE.md where content fully moved to AGENTS.md
- Keep package CLAUDE.md only where Claude-specific gotchas exist (data, ssl_certificates)
- Add CLAUDE.local.md to .gitignore

* feat: address Copilot review comments in PR #981

- Fix md_formatter.py docstring: was 'prettier', now correctly says 'markdownlint-cli2 --fix'
- Expand settings.json deny list to cover keystores (.jks, .keystore, .p12) and signing keys (.pem, .key, .p8)
- Fix AGENTS.md import groups: two '2.' entries corrected to sequential 1–6 numbering
- Fix packages/data/CLAUDE.md migration steps: misnumbered list corrected to 1–5
- Add working directory note to packages/data/AGENTS.md commands section
- Fix _web_socket_channel/AGENTS.md: 'pinned to' → 'constrained to' for caret constraint

* feat: remove webtrit_phone_keystores from deny list (dir is outside project scope)

* feat: configure gitignore, formatter, and analysis for generated files (#985)

* feat: configure gitignore, formatter, and analysis for generated files

- Add **/build/ and **/.claude/ to .gitignore
- Update lefthook pre-commit to skip *.g.dart / *.freezed.dart / *.gr.dart
- Replace melos fmt/fmt:check with find-based scripts that exclude generated files

* feat: update melos v7 integration across features, DAOs, and screenshots

* feat: allow feat/ as valid branch prefix alongside feature/ (#987)

* feat: allow feat/ as valid branch prefix alongside feature/

Update branch-name-check.sh and git-lint.yml to accept both feat/ and
feature/ prefixes. Add CONTRIBUTING.md documenting branch naming rules
and commit conventions.

* docs: fix table alignment in CONTRIBUTING.md

* docs: consolidate git conventions into CONTRIBUTING.md as single source of truth

- Remove .rules.md (referenced non-existent .rules/ directory)
- Update .github/copilot-instructions.md to reference CONTRIBUTING.md and AGENTS.md
- Add feat/ to branch pattern in copilot-instructions.md
- Fix pre-commit hook description in docs/development.md (dart format, not flutter analyze)
- Add CONTRIBUTING.md reference in docs/development.md
- Add docs/development.md pointer in CONTRIBUTING.md hooks section

* feat: favorites remote syncable (#984)

* refactor: cdr ui improvements, disconnect reason translations (#989)

* fix: use firstWhere instead of first in getAllContacts test to avoid ordering assumption (#991)

* fix: change contact_phones UNIQUE constraint to (number, label, contact_id) (#992)

* feat: change contact_phones UNIQUE constraint to (number, label, contact_id)

* feat: update ContactPhonesDao to use (number, label) pairs for stale-row deletion

* feat: persist per-label phone rows in ContactsLocalDataSource for DID-only accounts

* feat: add tests for DID-only contact phone handling and schema migration v20

* docs: add inline comment explaining grouping and merge logic in displayPhones

* refactor: rename lambda param p to phone in deleteOtherContactPhonesOfContactId call

* style: apply dart format to changed files

* docs: add DartDoc to deleteOtherContactPhonesOfContactId

* docs: add inline comments to deleteOtherContactPhonesOfContactId body

* style: replace non-ASCII arrow with hyphen in comment

* fix: use canonical label in displayPhones to prevent merged label from reaching favorites

* fix: recreate contact_phones via new table to preserve favorites FK reference

* test: add favorites FK integrity assertion for migration v20

* fix: resolve canonical ContactPhone by id before passing to favorites

* docs: document merged label display and canonical label favorites behavior

* refactor: replace ContactPhone display with flat fields in ContactPhoneDisplayEntry

- Replace synthetic ContactPhone display field with displayLabel (String)
  and displayFavorite (bool) - only the two fields that actually differ
  from the canonical phone
- Rename canonical field to phone for clarity
- Update displayPhones convenience getter to reconstruct ContactPhone
  from phone + flat display fields
- Update ContactPhoneTileAdapter to accept only primitives and closures,
  removing all model object dependencies
- Update ContactScreen loop to use displayPhoneEntries with closures
  capturing entry.phone directly
- Add widget tests for ContactPhoneTileAdapter covering all enable flags,
  callback wiring, transfer logic, and popup menu entries
- Add direct tests for displayPhoneEntries covering displayLabel,
  displayFavorite, and phone field contracts

* feat: melos exported env support (#993)

* fix: log records file stability improvements (#998)

* fix: guard File.delete() with exists() check in shareLogRecords

Prevents PathNotFoundException on Android 15 when the OS clears the
app cache directory under background memory pressure while the native
share sheet is open, causing the finally block to attempt deleting a
file that no longer exists (Crashlytics issue 25d22f4de9016c556b46cfacea29a20b).

* fix: delegate dispose Future in LogRecordsFileRepositoryImpl

Without awaiting or returning the Future from RotatingFileAppender.dispose(),
the file handle could remain unclosed after the caller awaited repository
disposal. Use direct return to delegate the Future without an async wrapper.

* fix: replace existsSync retry with async exists() in _getAllLogFilesWithRetry

existsSync() reads from the OS filesystem cache and can return false
immediately after forceFlush() even though the file is on disk.
Switching to async File.exists() forces a fresh stat() call that
bypasses the cache. Also reduces max wait from 10s (5x2s) to 1s (10x100ms).

* fix: guard forceFlush() with try/catch in readAllLogs

An I/O error during forceFlush() would propagate and abort readAllLogs
entirely, returning no logs to the caller. Catching the error allows
reading whatever files are already on disk.

* fix: guard forceFlush() with try/catch in cleanLogs

An I/O error during forceFlush() would abort cleanLogs entirely,
leaving stale log files on disk. Catching the error allows deletion
to proceed on whatever files are already present.

* fix: replace retry loop with async file discovery in log records

- Replace _getAllLogFilesWithRetry (10×100ms polling loop) with
  _getAllLogFilesAsync — single async pass using await file.exists()
  per rotation slot, bypassing OS filesystem cache without any delay
- Extend file discovery range to 0..keepRotateCount inclusive so
  rotated files (e.g. app_logs.log.1) are found even when base file
  is absent immediately after rotation
- Fix cleanLogs to use await _getAllLogFilesAsync() instead of
  synchronous getAllLogFiles() which relied on existsSync()
- Fix readAllLogs iteration order: remove files.reversed so newer
  file (rotation 0) is read before older rotated file (rotation 1)
- Remove redundant file.exists() guard inside readAllLogs loop since
  _getAllLogFilesAsync already guarantees file presence
- Add full test coverage: LogRecordsMemoryRepositoryImpl,
  ReadableRotatingFileAppender (readAllLogs + cleanLogs),
  LogRecordsFileRepositoryImpl — 26 tests total

* fix: update outdated comment in readAllLogs test

* feat: add Claude Code PostToolUse hooks (dart formatter + newline enforcer) (#995)

* fix: call dropped when user taps before app fully initializes (#997)

* fix: wait for signaling and registration before failing outgoing call

In __onCallPerformEventStarted, the early registration guard ran before
the signaling wait, causing calls to drop immediately when the user tapped
call before the socket initialized. Move the guard after the signaling wait
and extend the wait predicate to also require registration status to be
known (isHandshakeEstablished && isSignalingEstablished).

* fix: hold outgoing call as pending until routing state is available

When the user taps call before CallRoutingCubit has initialized (app just
launched, user info not yet fetched), wait for the first non-null routing
state instead of immediately failing. The call proceeds automatically once
routing state becomes available. If the cubit is disposed while waiting,
the call is silently dropped.

* fix: remove unused notification import from CallController

* test: add CallController.createCall unit tests

Cover immediate dispatch, pending wait, cubit disposal, and
CallUndefinedLineNotification scenarios.

* fix: remove unused optional params in _FakeCallRoutingState

* fix: remove unnecessary call_controller.dart import in test

* fix: address Copilot review — unawaited createCall and fast-fail on signaling failure

- Make createCall void by delegating to private _createCallAsync via unawaited, avoiding unawaited_futures lint at all call sites
- Add isFailure condition to signaling firstWhere predicate so outgoing calls fail immediately on signaling failure instead of waiting full timeout

* fix: update call_controller tests to use void createCall

Replace await controller.createCall(...) with call + await Future.delayed(Duration.zero)
to pump microtasks after createCall became void

* refactor: extract signaling wait predicate into named variables

* fix: use currentState instead of state for registration check after signaling wait

* fix: replace non-ASCII em dash with semicolon in comment

* refactor: remove redundant signalingConnected/registrationKnown vars, use CallState getters directly

* fix: await all addTrack calls before createOffer to prevent empty offer

* refactor: add TODO to provide CallController as singleton via RepositoryProvider

* fix: log warning when callRoutingCubit closes before routing state arrives

* refactor: add comment explaining callRoutingState await logic

* fix: add timeout to routing state wait and show NoInternetConnectionNotification on expiry

* refactor: extract _waitForRoutingState helper to clean up routing state await

* fix: catch unexpected errors from _createCallAsync and add timeout notification test

* fix: await reportNewIncomingCall in background FCM handler (#1001)

Without await the Pigeon IPC Future was fire-and-forget — the background
isolate was destroyed before the call reached the Kotlin side, so
PhoneConnectionService.startIncomingCall() was never invoked and the
incoming call UI never appeared.

* feat: provide CallController as singleton via RepositoryProvider in MainShell (#1000)

* feat: provide CallController as singleton via RepositoryProvider in MainShell

Register CallController once in MainShell widget tree after CallBloc,
CallRoutingCubit and NotificationsBloc are available. All seven call
sites now obtain the shared instance via late final field initializer
(context.read<CallController>()) instead of constructing a new object
per StatefulWidget.

* feat: replace RepositoryProvider with CallControllerScope InheritedWidget

RepositoryProvider is semantically for the data layer; CallController
is a UI-tier coordinator. Introduce CallControllerScope (InheritedWidget)
following the PresenceViewParams pattern already in the codebase.
All seven consumers now resolve the controller via CallControllerScope.of(context).

* fix: prevent stale CallController on MainShell rebuild

Store CallController in _MainShellState via ??= so it is created once
regardless of how many times build() reruns. Switch CallControllerScope.of
to getElementForInheritedWidgetOfExactType to avoid registering a rebuild
dependency that would never fire (updateShouldNotify is always false).

* feat: add doc comment for _callController field in _MainShellState

* fix: feature access mapper logic (#1007)

* fix: correct icon and text case assertions in integration tests (#1010)

* fix: correct icon and text case assertions in integration tests

* revert: remove accidentally committed logger from call_bloc.dart

* feat: add integration tests for webtrit_signaling keepalive and disconnect (#1009)

* feat: add integration tests for webtrit_signaling keepalive and disconnect

Add mock-based and live integration tests covering:
- Keepalive timeout (WebtritSignalingKeepaliveTransactionTimeoutException)
- Normal keepalive cycles (echo → timer restarts)
- Graceful disconnect (onDisconnect callback)
- Stream error (onError callback)
- Execute after disconnect (WebtritSignalingDisconnectedException)
- Request transaction timeout (non-keepalive variant)
- Live tests against a real server (skipped when credentials not set)
- Network simulation via pure-Dart WebSocket proxy with pause/resume
  to verify keepalive timeout fires on real packet drop

* feat: replace WebSocket proxy with raw TCP proxy in live signaling test

Drop packets at the TCP byte level instead of WebSocket frame level
for more realistic network simulation. The proxy rewrites the HTTP
Host header before forwarding so the server accepts the upgrade request.

* feat: extract TcpProxy into reusable _tcp_proxy package

Move the raw TCP proxy from the signaling test into a standalone
internal package packages/_tcp_proxy so it can be used as a
dev dependency in both webtrit_signaling tests and app-level tests.

* fix: address Copilot review comments on signaling tests and tcp proxy

- Fix expectLater() to pass Future directly instead of closure
- Make live test client nullable to prevent LateInitializationError in tearDown
- Use local signalingClient variable in each live test for clarity
- Replace localhost with 127.0.0.1 to avoid IPv6-first resolution issues
- Remove unconditional TLS certificate bypass in live test HTTP client
- Discard bytes while paused in _relayWithHostRewrite to prevent unbounded buffer growth
- Add 64 KB header size guard with socket teardown on overflow
- Add AGENTS.md, README.md, analysis_options.yaml to _tcp_proxy package

* fix: add lints dev_dependency to _tcp_proxy so analysis_options.yaml resolves

* fix: replace magic timing numbers with constants in integration tests

* docs: add DartDoc to _findCrlfCrlf explaining HTTP header boundary detection

* fix(signaling): reconnect silently on server force-close (code 4441) (#1012)

When the server sends disconnect code 4441 (controllerForceAttachClose)
it means two signaling sessions from the same account were open at the
same time — a race that can happen when a background push isolate is
still connected as the main engine comes to the foreground and
reconnects signaling.

Previously this code path set lastSignalingDisconnectCode, which
triggered CallStatus.connectIssue and showed a "Connection issue"
snackbar to the user even though the situation was transient and
self-healing.

Fix: on 4441, emit lastSignalingDisconnectCode=null (no connectIssue
UI), skip the notification, and reconnect with the fast 1s delay
instead of the default 3s. A warning log is emitted so the race remains
visible in logs for debugging.

* refactor: decouple AppLogger from LogzioLoggingService via RemoteLoggingService abstraction (#1011)

* refactor: decouple AppLogger from LogzioLoggingService via RemoteLoggingService abstraction

* refactor: move remoteMinLevel extraction to init to avoid cast in applyConfig

* refactor: add minLevel to RemoteLoggingService and remove cast from AppLogger

* refactor: reorder LogzioLoggingService fields to match RemoteLoggingService interface order

* refactor: remove labelsProvider from AppLogger state, pass labels explicitly

* refactor: rename regenerateRemoteLabels to updateRemoteLabels and dispose before reinitialize

* refactor: replace labels map with lazy callback in AppLogger to encapsulate labels retrieval

* fix: attach remote appender before applyConfig so early logs are forwarded to remote

* feat: enable and disable log anonymization with env (#1004)

* feat: enable and disable log anonymization with env

Added functionality to disable and enable log anonymization via environment variable

* refactor: simplify AnonymizationType to none/full enum with bool flag

Replace the mutable list-based anonymization approach with a single
AnonymizationType enum (none/full), removing the race condition risk
and simplifying the API surface.

* docs: document intentional anonymization scope in AppLogger

* fix: add @override annotation to setAnonymizationEnabled in LogzioLoggingService

---------

Co-authored-by: Dmytro Serdun <d.serdun@webtrit.com>

* fix: api healthcheck path (#1014)

* fix: use shared static logger in WebtritSignalingClient (#1013)

* fix: use shared static logger in WebtritSignalingClient with instance id prefix

* fix: restore custom logger support via inner constructor parameter

* fix: simplify logger to use fixed name without instance counter

* fix: remove custom logger parameter from inner constructor

* fix: signaling errors  (#1016)

* fix: snackbar duration

* fix: new persist option usage

* fix: reconnect message repeating

* fix: reconnect on keepalive timeout

* feat: user info cache (#1015)

* feat: main impl

* refactor: call to actions fix

* fix: call to actions provider

* docs: getAndListed explain

* fix: integration tests rescue (#1025)

* fix: migrate to 4.5

* fix: various fixes

* fix: remove log file from standart test

* fix: no audio if lone codec (#1028)

* fix: WebRTC signaling state guards — ICE restart, renegotiation, and glare (WT-986) (#1003)

* refactor: extract RenegotiationHandler with stable-state and concurrency guards

- Extract renegotiation logic from CallBloc into a standalone RenegotiationHandler class
- Add two stable-state guards: pre-offer check and TOCTOU guard after createOffer
- Add _isHandling/_pendingRetry flags to serialize concurrent onRenegotiationNeeded firings
- Catch WebtritSignalingErrorException for server-side error logging without swallowing
- Catch plain String errors (flutter_webrtc native) separately from Dart exceptions
- Add unit tests covering stable-state skip, concurrency serialization, and error paths
- Document server-mediated vs P2P topology constraints and Perfect Negotiation limitation

* fix: add signalingState guards and Perfect Negotiation rollback to call flow

ICE restart handler:
- Skip setLocalDescription when signalingState != stable to prevent native crash
- Log warning instead of silently skipping

Renegotiation / accepted handler:
- Guard setRemoteDescription(answer) against wrong state after glare resolution
- Log transceivers after setRemoteDescription for SDP debugging
- Catch String errors from setRemoteDescription to prevent unhandled exception escalation

Updating handler (Perfect Negotiation rollback):
- Pre-check signalingState for glare: if have-local-offer, roll back local offer before setRemoteDescription
- Catch String errors containing have-local-offer as fallback for stale flutter_webrtc signalingState cache
- Roll back and retry setRemoteDescription on confirmed glare

Renderer and state:
- Always refresh srcObject in RTCStreamView.didUpdateWidget to handle renegotiation-replaced tracks
- Fix remoteVideo getter to use logical OR (stream tracks || video flag) instead of short-circuit

Add call_state_test.dart covering ActiveCall equality and remoteVideo edge cases

* refactor: replace on String catch with typed RTC exceptions via RtcJsepErrorParser

* fix: guard RTCVideoRenderer srcObject assignment until initialize() completes (#1027)

* fix: guard RTCVideoRenderer srcObject assignment until initialize() completes

Prevents a crash where didUpdateWidget set srcObject before the renderer
was initialized, causing 'Call initialize before setting the stream'.

The _initialized flag ensures srcObject is only set after initialize()
resolves. Also always refreshes srcObject (not just on stream identity
change) to handle track replacement during renegotiation.

* fix: use setState and guard build before renderer initialization

Wrap _initialized and srcObject assignment in setState so the flag change
is properly synchronized with Flutter's build cycle. Guard build() to
avoid passing an uninitialized renderer to RTCVideoView — the
placeholderBuilder (or empty SizedBox) is shown until initialize()
completes.

* fix: update ExternalContactsSyncBloc tests to mock getAndListen instead of getLocalInfo (#1029)

* feat: handle 'voicemail_not_configured' error (#1005)

* feat: handle 'voicemail_not_configured' error

Added handling 'voicemail_not_configured' error from  voicemail api

* fix: properly handle voicemail_not_configured and endpoint_not_supported errors across all layers

- WebtritApiClient: skip SEVERE log for expected VoicemailNotConfiguredException / EndpointNotSupportedException
- VoicemailRepositoryImpl: add _featureSupported flag to skip API calls once feature is known unavailable; add _fetchingCompleter.future.ignore() to prevent unhandled future errors; suppress stack trace in warning log for expected exceptions; expose isFeatureSupported getter
- VoicemailRepository: add isFeatureSupported to abstract interface; EmptyVoicemailRepository returns false
- VoicemailCubit: check isFeatureSupported on init to immediately emit featureNotSupported without an API call
- SettingsBloc: catch expected exceptions from unawaited fetchVoicemails() to prevent runZonedGuarded propagation
- PollingService / ConnectivityLifecycleService: remove stack trace from WARNING logs in generic catch blocks

* fix: always allow navigation to voicemail screen regardless of feature support

Previously the settings tile was disabled with reduced opacity when voicemail
was not configured, which was confusing. Now the tile is always tappable and
the voicemail screen shows a placeholder explaining the reason.

* fix: move VoicemailCubit provider from SettingsScreenPage to VoicemailScreenPage

VoicemailCubit was provided at the settings level but consumed in a separate
route, causing ProviderNotFoundException on navigation. Moving it to
VoicemailScreenPage makes the provider scope match the consumer.

* fix: add missing call feature import to VoicemailScreenPage

* fix: remove unused stack trace variables from catch blocks in polling and connectivity services

* fix: address Copilot review comments

- VoicemailCubit: guard watchVoicemails subscription against overwriting featureNotSupported state
- VoicemailNotConfiguredException: pass token and error fields to super; remove commented-out placeholder
- WebtritApiClient: log xRequestId (never null) instead of requestId parameter; pass token/error to VoicemailNotConfiguredException
- SettingsTile: add assert that opacity is in [0.0, 1.0]

---------

Co-authored-by: Dmytro Serdun <d.serdun@webtrit.com>

* feat: add toJson() to all Event subclasses and fix NotifyEvent constructor inconsistency (#1030)

* fix: suppress transient network error SnackBar in RegisterStatusCubit (#1031)

Auto-triggered fetches (startup and connectivity restore) now use
_fetchStatusSilently(), which skips handleError for SocketException,
TimeoutException, and TlsException. The cubit retries automatically
on the next connectivity change, so surfacing these transient failures
to the user is misleading. User-initiated fetchStatus() retains the
original behaviour and still calls handleError for all errors.

* fix: add 10s timeout to reportNewIncomingCall in background message handler (WT-1061) (#1032)

Telecom on affected devices can become overloaded with phantom PhoneAccount
registrations, causing reportNewIncomingCall() to hang indefinitely. This
keeps FlutterFirebaseMessagingBackgroundService alive and blocks subsequent
cold starts. Adding a 10s timeout bounds the handler lifetime and logs a
warning when Telecom is slow to respond.

* fix: add 5s timeout to getInitialMessage() to prevent splash freeze (WT-1061) (#1033)

* refactor: remove tryUse from AppDatabaseScope, migrate callers to useOrNull (#1034)

* fix: eliminate write-write SQLite contention via shared DriftIsolate server (WT-1061) (#1035)

* fix: eliminate write-write SQLite contention via shared DriftIsolate server (WT-1061)

Spawns a single dedicated DriftIsolate server in the main isolate bootstrap
and registers its SendPort in IsolateNameServer under a fixed key.

Background isolates (FCM handler, WorkManager) now connect to the same server
via IsolateDatabase.connectOrCreate(), which looks up the port and creates a
client connection — falling back to a direct NativeDatabase connection when the
main app is not running (cold push with no foreground app).

All writes are serialized through the single server isolate, making
write-write SQLITE_BUSY (code=5) between concurrent isolates impossible.

Changes:
- app_database: add createAppDatabaseNative() for synchronous NativeDatabase
  creation inside the server isolate (no createInBackground needed)
- IsolateDatabase: add spawnServer(), connectOrCreate(), kDbPortName
- AppDatabaseScope.use(): connect via connectOrCreate() instead of create()
- bootstrap(): spawn DriftIsolate server, register DriftIsolate in InstanceRegistry
- AppDatabaseLifecycleHolder: connect to DriftIsolate, shutdown server on dispose

* fix: address Copilot review — robust spawnServer error handling and stale port cleanup (WT-1061)

* test: add integration tests for IsolateDatabase stale port handling (WT-1061)

* refactor: introduce SignalingModule stream abstraction (phase 1) (#1024)

* refactor: introduce SignalingModule stream abstraction (phase 1)

Replace SignalingManager callback-based API with SignalingModule — a
sealed-event broadcast stream that owns the WebtritSignalingClient
lifecycle without any BLoC, CallState, or UI dependency.

Key changes:
- Add SignalingModule with fire-and-forget connect(), disconnect(),
  dispose() and a sealed SignalingModuleEvent hierarchy
  (Connecting, Connected, ConnectionFailed, Disconnecting,
  Disconnected, HandshakeReceived, ProtocolEvent)
- Add isRepeated deduplication on ConnectionFailed to suppress
  repeated identical error notifications
- Map disconnect codes to recommendedReconnectDelay:
  4441 → Duration.zero, protocolError → null, all others → 3 s
- Migrate CallBloc from direct WebtritSignalingClient callbacks to
  SignalingModule stream subscription; new _SignalingClientEvent
  variants: connecting, connected, failed, disconnecting, disconnected
- Migrate IsolateManager (Push + Foreground) to SignalingModule,
  replacing SignalingManager; add connectivity monitoring and pending
  request queue inside IsolateManager
- Construct SignalingModule in main_shell.dart and inject into CallBloc
- Delete SignalingManager and remove its export from common.dart
- Add 31 unit and integration tests for SignalingModule

* fix(test): update ExternalContactsSyncBloc tests for getAndListen API

The BLoC was updated to call userRepository.getAndListen() instead of
getLocalInfo(), but the mocks were never updated. Fix the setUp mock
and correct the RefreshFailure test to use load() failure (which is
the actual trigger for that state) rather than userRepository failure.

* fix(test): rename local function to avoid leading underscore lint warning

* docs: translate signaling architecture doc to English

* docs: remove phase 1 requirements planning doc from repo

* refactor: remove coreUrl/tenantId/token/trustedCertificates from CallBloc

These four fields were passed through CallBloc only to construct
SignalingModule internally. Now that SignalingModule is constructed
externally and injected via the constructor, the fields are dead code.
Remove them and the corresponding import from ssl_certificates.

* fix: address Copilot review comments on SignalingModule/IsolateManager

- Guard delayed reconnect callbacks with signalingClient == null check to
  avoid tearing down a healthy connection that connected during the delay
- Populate _incomingCallEvents from handshake and protocol events so
  _findIncomingEventLog returns real caller data instead of null
- Use disconnect() instead of dispose() in handleLifecycleStatus so the
  module remains reusable when the app returns to the foreground
- Fix post-dispose connect() test to actually subscribe to the event stream
  and assert Connecting/Connected events are absent after dispose

* feat: replay session events to late subscribers in SignalingModule

Adds a per-subscriber replay buffer so that consumers created after
connect() (e.g. CallBloc constructed after SignalingModule already
connected and received a handshake) do not miss any events from the
current session.

- events getter now returns a single-subscription stream that first
  replays all events buffered since the last connect() call, then
  pipes live events from the broadcast controller
- connect() clears the buffer so late subscribers see only the
  current session, not stale events from previous reconnect cycles
- dispose() also clears the buffer on teardown
- Uses sync: true on the intermediate StreamController to avoid an
  extra async hop and keep delivery ordering consistent with callers
  that await module operations
- Adds two integration tests covering the late-subscriber replay
  and the buffer-clear-on-reconnect behaviours

* feat: connect SignalingModule early in initState to reduce call setup latency

SignalingModule is now created and connected in _MainShellState.initState(),
running the WebSocket handshake in parallel while the widget tree and
CallBloc are being built. When CallBloc is eventually created it subscribes
to the replay stream and receives all buffered session events without missing
anything.

_MainShellState.dispose() owns the module lifecycle; CallBloc.close()
still calls dispose() on the module (idempotent, safe).

* docs: update signaling architecture doc with layer descriptions and diagrams

* fix: add concurrency lock to _connectAsync to prevent parallel connects

* fix: await disconnect ack in dispose() to prevent SignalingDisconnected drop on race

* fix: suppress reconnect hint on intentional disconnect to prevent spurious reconnect

* fix: remove SignalingModule.dispose() from CallBloc.close() — ownership belongs to MainShellState

* fix: snapshot buffer before live subscribe to prevent replay duplicates in events getter

* fix: store reconnect Timer in IsolateManager so it can be cancelled on close()

* fix: forward recommendedReconnectDelay from SignalingDisconnected to _scheduleReconnect in CallBloc

* fix: suppress _onDisconnect after _onError to prevent double reconnect scheduling

* fix: exclude SignalingProtocolEvent from session buffer to prevent unbounded growth

* fix: replace force-unwrap of session.coreUrl/token with null-safe logout in initState

* fix: remove performEndCall early return so pre-handshake declines are queued

* fix: close liveController on subscription cancel to prevent StreamController leak

* fix: use _networkNone state instead of stale results snapshot in connectivity timer closure

* docs: fix _scheduleReconnectIfNeeded → _scheduleReconnect in signaling architecture doc

* test: replace Future.delayed(Duration.zero) with pumpEventQueue() in signaling module tests

* fix: make _controller sync:true to eliminate async-dispatch event duplication

* docs: clarify disconnect() docstring — SignalingDisconnected is callback-driven, not synchronous

* fix: wrap _signalingModule.disconnect() in unawaited() in _disconnectInitiated

* fix: remove unused shouldReconnect variable from __onSignalingClientEventDisconnected

* test: add 8 tests to reach 100% SignalingModule coverage

- concurrent connect() dropped while factory in-flight (_connecting guard)
- intentional disconnect() emits SignalingDisconnected with null delay
- disconnect() passes goingAway code to the underlying client
- _onError suppresses subsequent _onDisconnect (_errorHandled flag)
- SignalingProtocolEvent excluded from replay buffer
- cancelled subscription receives no further events
- dispose() awaits disconnect ack before closing the stream
- _onHandshake/_onEvent are no-ops after dispose()

* test: add scenario-driven SignalingModule tests from CallBloc usage analysis

Covers scenarios observed in CallBloc's signaling subscription:

internet dropped mid-session:
- _onError after handshake → ConnectionFailed not Disconnected, signalingClient cleared
- Unexpected socket close (null code) → Disconnected with kSignalingClientReconnectDelay
- ConnectionFailed buffered so late subscribers reconstruct last-known failure

handshake not completed:
- Disconnect before handshake → no HandshakeReceived in buffer, Disconnected with delay
- Late subscriber after no-handshake disconnect → gets Connecting+Connected+Disconnected only
- Error before handshake → ConnectionFailed buffered, no HandshakeReceived
- Reconnect after no-handshake failure delivers fresh session events

late subscriber mid-session:
- Factory still pending → gets Connecting from buffer, Connected arrives live
- After full connect+handshake → all three lifecycle events replayed, no protocol events

disconnect() robustness:
- client.disconnect() throws → dispose() still completes without hanging
- Second disconnect() with no active client is a silent no-op

* fix: restore callkeep_signaling_status_converter.dart lost during rebase

* fix: remove unused fields in _ThrowingDisconnectClient test helper

* fix: use normalClosure (1000) instead of goingAway (1001) when client disconnects WebSocket

* test: update disconnect test to expect normalClosure (1000) instead of goingAway (1001)

* fix: address Copilot review comments in SignalingModule and IsolateManager

- Fix doc comment on events getter: protocol events are not replayed, only lifecycle/handshake events
- Guard connect() buffer clear behind _connecting check to avoid clearing on redundant calls
- Remove stale comment in _onDisconnect that contradicted the !_disposed guard
- Treat empty connectivity result as offline in _monitorConnectivity (results.isEmpty || any(none))
- Treat empty connectivity result as offline in performAnswerCall (isNotEmpty && !contains(none))

* fix: address post-review issues in SignalingModule and IsolateManager

- isolate_manager: fix connectivityNoneCounter reset — error now fires
  exactly once at maxConnectivityNoneRepeats; subsequent none-events are
  silently ignored until connectivity is restored and counter resets to 0

- main_shell: split SignalingModule construction into valid/invalid-creds
  branches to remove ?? '' fallbacks and make intent explicit

- signaling_module: document sync:true reentrancy assumption, single-use
  constraint on events getter, and _errorHandled ordering invariant

* revert: restore main_shell.dart SignalingModule construction with ?? '' fallback

The split-branch approach still created a module with empty strings in the
null-creds case — identical in behaviour to the original. Reverted to the
original form which is honest about the fallback until a proper nullable
refactor is done.

* fix: remove dead null-guard in MainShellState.initState for SignalingModule

The router guard (onMainShellRouteGuardNavigation) redirects to login
when state.status != authenticated, so coreUrl and token are always
non-null when MainShell is mounted. Replace ?? '' fallbacks and the
unreachable null-branch with direct ! unwraps.

* docs: sync signaling_architecture_target.md and call_architecture.md with current code

- signaling_architecture_target: add timer cancellation to IsolateManager
  and CallBloc _scheduleReconnect snippets (Future.delayed → Timer with cancel)
- signaling_architecture_target: add missing SignalingDisconnecting case to
  CallBloc subscription snippet
- call_architecture: update ownership — CallBloc owns SignalingModule, not
  WebtritSignalingClient directly

* fix: replace non-ASCII characters with ASCII equivalents in Dart sources

Replace em dash (U+2014) with '-' and right arrow (U+2192) with '->'
in comments and test descriptions across signaling_module.dart,
isolate_manager.dart, signaling_module_test.dart, and call_bloc.dart.

* fix: incorrect styling of status bar on app start (#1006)

Fixed status bar rendering with incorrect styling on initial app launch when theme is light

* feat: show progress indicator while sharing logs (#1036)

* feat: show progress indicator while sharing logs

Co-Authored-By: Dmytro Serdun <serdun@webtrit.com>

* refactor: replace inline SizedBox+CircularProgressIndicator with SizedCircularProgressIndicator

---------

Co-authored-by: Dmytro Serdun <serdun@webtrit.com>

* fix: upgrade to video resets hold (#1038)

* fix: media settings parsing (#1039)

* fix: call drops after theme or lang change (#1041)

* fix: cannot make calls after blind transfer — skip hangup + reconnect safety net (WT-1214) (#1040)

* fix: trigger reconnect when starting outgoing call with no signaling (WT-1214)

After a blind transfer, the signaling WebSocket is closed with code 4610
and the disconnect is marked intentional by SignalingModule, so no
reconnect is scheduled. Subsequent outgoing call attempts enter
outgoingConnectingToSignaling and wait passively — neither signalingReady
nor signalingFailed ever fires, causing the call to fail on timeout.

Add _scheduleReconnect(Duration.zero) at the start of the waiting block
so that initiating an outgoing call always recovers the signaling
connection, regardless of whether the previous disconnect was intentional.

* fix: skip hangup after successful blind transfer to avoid 4610 disconnect (WT-1214)

When a blind transfer completes (NOTIFY SIP/2.0 200 OK +
subscription_state: terminated), the SIP dialog is already closed
server-side via REFER. Sending a hangup request on the freed dialog
causes the server to close the WebSocket with code 4610 ("call request
on wrong line error"), triggering an unintended signaling disconnect.

Check the call's transfer state: if it is Transfering(fromBlindTransfer:
true), skip the hangup request and clean up the peer connection locally
only. This removes the root cause of the 4610 disconnect that led to
signaling not being reconnected for subsequent outgoing calls.

* test: cover blind-transfer hangup skip and 4610 reconnect hint

Add tests for two fixes from WT-1214:

- Transfer — isBlindTransferCompleted detection (call_state_test.dart):
  verifies the switch pattern that decides whether to skip the hangup
  request after a blind transfer. Covers Transfering(fromBlindTransfer:
  true/false), earlier transfer states, and null.

- SignalingModule — requestCallIdError (4610) reconnect hint
  (signaling_module_test.dart): verifies that a non-intentional 4610
  carries a non-null recommendedReconnectDelay (reconnect scheduled),
  while an intentional disconnect() followed by a server 4610 emits
  null (reconnect suppressed — the scenario that triggered WT-1214).

* refactor: rename isBlindTransferCompleted → isBlindTransferInTransferingState

Transfering state in the model means "server started to process the
transfer", not "transfer completed". Rename the local variable and update
the surrounding comments and test group names to match the actual Transfer
model semantics, avoiding misinterpretation of the hangup guard.

Addresses Copilot review comments on PR #1040.

* fix: call or transfer to myself handling (#1046)

* fix: hide video for held call (#1048)

* fix: hide video for held call

* fix: tap area

* fix: transfer to same recipient (#1049)

* refactor: extract SignalingModuleInterface; migrate CallBloc and IsolateManager; extract toLinesState (#1045)

* refactor: extract SignalingModuleInterface; decouple IsolateManager from SignalingModule

Add local SignalingModuleInterface abstract class to signaling_module.dart
with the contract needed by IsolateManager: events, isConnected, connect(),
disconnect(), execute(Request), dispose().

SignalingModule implements the interface, gaining isConnected and execute()
alongside the existing signalingClient getter (kept for backward compat).

IsolateManager field type changed from SignalingModule to SignalingModuleInterface.
All signalingClient null-checks replaced with isConnected; direct client.execute()
calls replaced with module.execute(). IsolateManager no longer depends on the
concrete class, making it ready for a plugin-backed implementation.

* fix: decouple NetworkCubit from WebtritSignalingService; fix push dedup race

NetworkCubit held a concrete WebtritSignalingService only to call
updateMode(). Replaced with a Future<void> Function(SignalingServiceMode)
callback so the cubit has no plugin dependency and is mock-testable.
Call site passes WebtritSignalingService().updateMode as a tear-off.

_onCallPushEventIncoming checked the incomingFromOffer guard before
awaiting contactNameResolver.resolveWithNumber(). The signaling path
could create an ActiveCall during that async gap, causing both paths
to emit separate entries for the same callId. Added a post-await guard
that checks for any existing ActiveCall with the same callId before emitting.

* fix: prevent premature call routing state emission before signaling handshake (#1044)

LinesState.blank() is emitted at app startup before the signaling handshake
arrives. combineLatest fires immediately with cached UserInfo + blank LinesState,
causing CallRoutingCubit to emit a non-null state with empty mainLines.
CallController then skips _waitForRoutingState() and fails with
"no idle lines available".

Fix: add LinesState.isBlank discriminator (guestLine == null is an unambiguous
pre-handshake marker — CallBloc always sets guestLine to non-null after any
handshake). Return null from _combineInfo when linesState.isBlank so the cubit
stays in its unready state and CallController waits correctly.

* fix: preserve LinesState.blank in onChange until signaling handshake arrives

The previous fix relied on guestLine == null as a pre-handshake discriminator,
but CallBloc.onChange always set guestLine = LineState.idle regardless of
linesCount, overwriting LinesState.blank() almost immediately after startup.

Root cause: onChange fired with linesCount = 0 on any early state change
(e.g. connecting status) and produced LinesState([], LineState.idle).
isBlank returned false, so _combineInfo emitted a non-null CallRoutingState
with empty mainLines, and hasIdleMainLine = false blocked the call.

Fix: when linesCount == 0 (handshake not yet received), onChange now stores
LinesState.blank() explicitly. Once the handshake sets linesCount > 0,
normal LinesState with non-null guestLine is produced as before.

* fix: remove unused models import from NetworkScreenPage

* fix: wire BackgroundSignalingBootstrapService into NetworkCubit callback

* refactor: migrate CallBloc to SignalingModuleInterface

Apply the same interface migration already done for IsolateManager:
- change _signalingModule field/param type from SignalingModule to SignalingModuleInterface
- replace signalingClient?.execute() with execute() from the interface
- replace signalingClient != null checks with isConnected

* fix: address Copilot review comments on signaling module interface

- Handle null execute() in _executePendingRequests: complete completer
  with error and clean up instead of leaving request to time out silently
- Handle null execute() in _sendRequest: log warning and return early
  instead of awaiting null when module disconnects after isConnected check
- Fix linesCount == 0 guard: use isHandshakeEstablished to distinguish…
SERDUN added a commit that referenced this pull request May 22, 2026
* fix: skip static payloads sanitizing if no rtpmap (#968)

* fix: media preset translation (#969)

* fix: fix splash config generation and add android_12 image support (#865)

- Escape # in Makefile color variables to prevent shell comment issues
- Remove redundant shell parameter expansion, use Make-only expansion
- Add ANDROID_12_SPLASH_IMAGE variable for separate Android 12 splash
- Use single quotes in echo to avoid shell interpretation problems

Co-authored-by: Dmytro Serdun <d.serdun@webtrit.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* chore: add diagnostic logging for recents number mismatch (#970)

Add hash-based and raw value logging to trace why recent calls list
shows two different phone numbers per entry. Raw values are visible
when Logz.io anonymization is disabled, hashes work with it enabled.

* feat: add support a blurred appbar surface (#971)

* feat: add BlurredSurface widget and apply blur effect to all app bars

Extract common ClipRect+BackdropFilter pattern into reusable BlurredSurface widget.
Apply consistent blur background with extendBodyBehindAppBar to all screens
using MainAppBar or AppBar. Remove isComplexBackground conditional logic.

* refactor: standardize MainAppBar field order across screens

* feat: apply blurred appbar surface to ConversationsScreen

* fix: remove unused theme/theme.dart imports

* fix: add top padding to prevent content overlap with blurred appbar

Screens using extendBodyBehindAppBar lacked compensating padding,
causing body content to be hidden behind the blurred AppBar. Added
appropriate top padding to recents, favorites, about, contacts, and
CDR screens consistent with the existing settings screen approach.

* feat(screenshots): make IgnorePointer configurable in ScreenshotApp (#972)

Add ignorePointer parameter (default true) to allow enabling interaction
for specific screenshots that require it.

* feat: add screenshot mocks and docs for all features (#973)

* feat: add screenshot mocks for all features

Add ~20 new screenshot definitions covering all user-visible screens:
- Core flows: contact, chat conversation, SMS conversation, system
  notifications, recent CDRs, number CDRs, call log
- Settings: network, language, diagnostic, caller ID, presence,
  theme mode, voicemail
- Login: switch screen
- Utility: log records console, contacts agreement, teardown,
  permissions, user agreement

Includes shared mock data, mock blocs/cubits/repositories,
screenshot widgets, router entries, and integration test registrations.

* docs: add screenshots package documentation

Add docs for screenshots architecture, mock reference, data reference,
and step-by-step guide for adding new screenshots. Update README with
links to the new documentation.

* fix: address review feedback for screenshot mocks and docs

- Add provider as direct dependency in screenshots/pubspec.yaml
  and remove ignore: depend_on_referenced_packages from 7 files
- Fix mock type/factory mismatches in mocks.md documentation
- Fix broken code formatting in mocks.md and adding_screenshots.md
- Remove shadowed ChatTypingCubit/SmsTypingCubit BlocProviders
  from conversation screenshot wrappers
- Guard registerFallbackValue with static flag to prevent
  duplicate registration
- Fix duplicated phrase in README.md Firebase Hosting section
- Document both flutter test and flutter drive workflows in
  overview.md

* feat: update keystores path to new applications subdirectory (#974)

* feat: add text style background decoration support (#975)

* feat: add backgroundBorderRadius and backgroundPadding to TextStyleConfig

* feat: add greetingTextStyle to LoginModeSelectPageConfig

* feat: add ExtendedText widget with background decoration support

* feat: add greetingTextStyle to login.modeSelect docs

* feat: add tests for ExtendedText widget

* feat: update theme template configs with missing components and fixes (#976)

- Fix calStatuses typo to callStatuses in both widget configs
- Add missing widget sections as examples: button, group, bar, input, text, confirmDialog
- Fix non-existent dark SVG asset references to use existing logos
- Remove unsupported fontFeatures from page text styles
- Clean up dark page config duplicate keys in login.switchPage
- Add greetingTextStyle for dark login modeSelect visibility
- Update dark widget decoration gradient config

* refactor: replace deprecated GradientColorsConfig with PageBackground (#977)

* refactor: replace deprecated GradientColorsConfig with PageBackground

Remove GradientColorsConfig, DecorationConfig, GradientsStyleFactory,
and Gradients theme extension. Migrate login and call screens to use
PageBackground via ThemedScaffold.

* refactor: enable gradient backgrounds for login and call page configs

Activate background for login modeSelect and add gradient background
to dialing section using colors from the removed GradientColorsConfig.

* refactor: adjust dark login gradient to contrast with buttons

Change bottom gradient color from #000000 to #0A1929 to avoid
blending with neutralOnDark button surface color.

* refactor: reverse dark login gradient direction

Start with dark color on top, lighter blue on bottom to keep
contrast with buttons at the bottom of the screen.

* refactor: rename misleading onSurface variable to surfaceColor

* feat: appBar blurred surface config and theme updates (#979)

* feat: extract appBar background color and blurred surface to config pipeline

Add configurable appBarBackgroundColor and appBarBlurredSurface (color, sigmaX, sigmaY)
to the theme config→style pipeline across all 7 main screens, replacing hardcoded values.

* feat: update page configuration docs with appBar fields

Add Common page fields section documenting appBarBackgroundColor
and appBarBlurredSurface, update Keypad page example.

* feat: remove appBarBackgroundColor from config pipeline

AppBar backgroundColor is already handled by MainAppBar fallback chain
(explicit → appBarTheme → canvasColor.withAlpha). Remove redundant
appBarBackgroundColor from BasePageConfig, all page configs, screen styles,
factories, and screens to avoid confusion.

* feat: add BlurredSurface.fromStyle factory and simplify screen usage

Replace inline BlurredSurface construction with fromStyle() factory
that returns null when no config is present (no blur applied) or a
configured BlurredSurface with resolved sigma defaults of 10.

* feat: enable appBar blur and activate widget config sections

- Add appBarBlurredSurface to all 7 page sections (dark/light)
- Activate previously ignored widget config sections (_bar, _button,
  _group, _input, _text -> remove underscore prefix)
- Set appBar backgroundColor/surfaceTintColor to transparent for blur
- Fix tab indicator label contrast and remove divider line

* feat: update light theme appBarBlurredSurface color to soft blue-white

* feat: set dark status bar icons for light theme appBar

* feat: fix appBarBlurredSurface sigma defaults and add tests

- Make sigmaX/sigmaY nullable in BlurredSurfaceConfig so omitted fields
  resolve to 10 via BlurredSurface.fromStyle instead of staying 0
- Regenerate Freezed/JSON for BlurredSurfaceConfig
- Restore blur on RecentCdrs and SystemNotifications screens by passing
  sigmaX: 10, sigmaY: 10 explicitly to const BlurredSurface()
- Explicit Colors.transparent fallback in BlurredSurface Container child
- Update docs to reflect null defaults with 10 resolved at widget layer
- Add BlurredSurface.fromStyle widget tests (4 cases)

* fix: system back button blocked in EmbeddedRequestErrorDialog (#935)

- Remove canPop: false from EmbeddedRequestErrorDialog — default is true,
  and blocking pop prevented the system back button from working in
  Settings → Terms & Conditions when the page failed to load (WT-890).
- Add onPopInvokedWithResult to call onBack when system back dismisses
  the dialog in Mode B (pushed route), ensuring _errorDialogShown is reset.
- Reset _errorDialogShown in _handleEmbeddedErrorState when error is
  cleared, so repeated errors after system-back dismissal are shown again.

Co-authored-by: Dmytro Serdun <d.serdun@webtrit.com>

* fix: prevent keepalive write-after-close and harden transaction lifecycle (#870)

* fix: prevent keepalive write-after-close and add regression test

* fix: clean up transaction on send failure in _executeTransaction

Move _addMessage inside the try-catch block so that if writing to a
closed socket throws WebtritSignalingBadStateException, the transaction
is properly removed from the _transactions map instead of leaking.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* test: add transaction cleanup and keepalive timer tests

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: add comment for closeCode guard in keepalive loop

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: address Copilot review comments on keepalive integration tests

- Fix compile error: replace called(greaterThan(0)) with called(1)
- Fix lint warning: await streamController.close() in tearDown
- Fix false-positive test: move closeCode mock before flushMicrotasks
  and add verify closeCode called(2) to catch timer-restart regression

---------

Co-authored-by: Dmytro Serdun <d.serdun@webtrit.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* fix: fixed app is locked in Bluetooth call profile after first call (#982)

* fix: fixed app is locked in Bluetooth call profile after first call

Fixed situation when user returns to YouTube or music playback, the audio remains degraded, like during a call. This happens only on Android.

* fix: improve comment for _onLastCallEnded to cover iOS and Android audio routing

---------

Co-authored-by: Dmytro Serdun <d.serdun@webtrit.com>

* feat: add melos v7 integration (#980)

* feat: add melos integration with codegen, test, format, and dependency scripts

* feat: fix melos scripts syntax for v7 and rename format to fmt

* feat: configure melos v7 with workspace and scripts in pubspec.yaml

- Add workspace: list to pubspec.yaml for Dart pub workspace / Melos v7 package discovery
- Add melos: section to pubspec.yaml with scripts for codegen, tests, formatting, dependencies
- Add resolution: workspace to all sub-package pubspec.yaml files
- Fix ssl_certificates publish_to typo (none; → 'none')
- Update screenshots SDK constraint to ^3.8.0
- Remove melos.yaml (ignored by Melos v7 — config lives in pubspec.yaml)

* feat: include root package in melos workspace via useRootAsPackage

* fix: replace non-ASCII em dashes with hyphens in pubspec.yaml comments

* feat: migrate makefile targets to melos scripts and mark deprecated

* fix: replace __ with _ in screenshots to fix unnecessary_underscores lint

* fix: rename melos script from run to start to avoid CLI conflict

The melos v7 CLI command `melos run <script>` conflicted with the script
named `run`. When running `melos run fmt`, melos treated `run` as the
script name and `fmt` as an argument, causing flutter to look for a file
named `fmt` instead of formatting code.

Renaming the script to `start` (and `run:ios` to `start:ios`) resolves
the ambiguity. Use `melos run start` to launch the app.

* feat: add melos smoke tests and update docs with melos commands

- Add tool/scripts/melos_smoke_test.sh for smoke testing safe scripts
- Add smoke:test melos script to pubspec.yaml
- Rename start to start:android for clarity
- Replace Makefile references with melos commands in docs/build.md
- Rewrite docs/make_file.md as full melos commands reference
- Update README.md to link to Melos Commands

* fix: address Copilot review comments

- Switch get/upgrade/outdated melos scripts from exec to run
  to correctly resolve from workspace root (not per-package)
- Align screenshots flutter constraint with workspace root (^3.41.2)
  to fix sdk/flutter constraint inconsistency (Dart 3.8 requires Flutter 3.41+)

* fix: reverse date divider in chat (#983)

Placed date divider before bunch of messages. Also changed displaying date in this divider. Added displaying options "Today", "Yesterday", day of week, like "Monday", and date in E, d MMM format, like "Tue, 6 Jan" , depending on comparison between date of message and today's date.

* feat: claude code setup (#981)

* feat: replace .rules with CLAUDE.md for Claude Code

* feat: configure .claude with hooks and allowed commands

* feat: add melos usage rules and update common commands

* feat: reduce CLAUDE.md duplication and soften melos usage rules

* feat: add webtrit_callkeep docs and package-level CLAUDE.md files

* feat: document call architecture, flows, isolates, and key patterns in CLAUDE.md

* feat: restructure AI agent memory files (AGENTS.md + CLAUDE.md split)

- Add root AGENTS.md (<100 lines): universal instructions for any AI tool
- Slim root CLAUDE.md (<50 lines): @imports + Claude-specific gotchas only
- Extract CallBloc architecture to docs/call_architecture.md
- Add AGENTS.md for all 10 packages (shared content, any agent)
- Remove package CLAUDE.md where content fully moved to AGENTS.md
- Keep package CLAUDE.md only where Claude-specific gotchas exist (data, ssl_certificates)
- Add CLAUDE.local.md to .gitignore

* feat: address Copilot review comments in PR #981

- Fix md_formatter.py docstring: was 'prettier', now correctly says 'markdownlint-cli2 --fix'
- Expand settings.json deny list to cover keystores (.jks, .keystore, .p12) and signing keys (.pem, .key, .p8)
- Fix AGENTS.md import groups: two '2.' entries corrected to sequential 1–6 numbering
- Fix packages/data/CLAUDE.md migration steps: misnumbered list corrected to 1–5
- Add working directory note to packages/data/AGENTS.md commands section
- Fix _web_socket_channel/AGENTS.md: 'pinned to' → 'constrained to' for caret constraint

* feat: remove webtrit_phone_keystores from deny list (dir is outside project scope)

* feat: configure gitignore, formatter, and analysis for generated files (#985)

* feat: configure gitignore, formatter, and analysis for generated files

- Add **/build/ and **/.claude/ to .gitignore
- Update lefthook pre-commit to skip *.g.dart / *.freezed.dart / *.gr.dart
- Replace melos fmt/fmt:check with find-based scripts that exclude generated files

* feat: update melos v7 integration across features, DAOs, and screenshots

* feat: allow feat/ as valid branch prefix alongside feature/ (#987)

* feat: allow feat/ as valid branch prefix alongside feature/

Update branch-name-check.sh and git-lint.yml to accept both feat/ and
feature/ prefixes. Add CONTRIBUTING.md documenting branch naming rules
and commit conventions.

* docs: fix table alignment in CONTRIBUTING.md

* docs: consolidate git conventions into CONTRIBUTING.md as single source of truth

- Remove .rules.md (referenced non-existent .rules/ directory)
- Update .github/copilot-instructions.md to reference CONTRIBUTING.md and AGENTS.md
- Add feat/ to branch pattern in copilot-instructions.md
- Fix pre-commit hook description in docs/development.md (dart format, not flutter analyze)
- Add CONTRIBUTING.md reference in docs/development.md
- Add docs/development.md pointer in CONTRIBUTING.md hooks section

* feat: favorites remote syncable (#984)

* refactor: cdr ui improvements, disconnect reason translations (#989)

* fix: use firstWhere instead of first in getAllContacts test to avoid ordering assumption (#991)

* fix: change contact_phones UNIQUE constraint to (number, label, contact_id) (#992)

* feat: change contact_phones UNIQUE constraint to (number, label, contact_id)

* feat: update ContactPhonesDao to use (number, label) pairs for stale-row deletion

* feat: persist per-label phone rows in ContactsLocalDataSource for DID-only accounts

* feat: add tests for DID-only contact phone handling and schema migration v20

* docs: add inline comment explaining grouping and merge logic in displayPhones

* refactor: rename lambda param p to phone in deleteOtherContactPhonesOfContactId call

* style: apply dart format to changed files

* docs: add DartDoc to deleteOtherContactPhonesOfContactId

* docs: add inline comments to deleteOtherContactPhonesOfContactId body

* style: replace non-ASCII arrow with hyphen in comment

* fix: use canonical label in displayPhones to prevent merged label from reaching favorites

* fix: recreate contact_phones via new table to preserve favorites FK reference

* test: add favorites FK integrity assertion for migration v20

* fix: resolve canonical ContactPhone by id before passing to favorites

* docs: document merged label display and canonical label favorites behavior

* refactor: replace ContactPhone display with flat fields in ContactPhoneDisplayEntry

- Replace synthetic ContactPhone display field with displayLabel (String)
  and displayFavorite (bool) - only the two fields that actually differ
  from the canonical phone
- Rename canonical field to phone for clarity
- Update displayPhones convenience getter to reconstruct ContactPhone
  from phone + flat display fields
- Update ContactPhoneTileAdapter to accept only primitives and closures,
  removing all model object dependencies
- Update ContactScreen loop to use displayPhoneEntries with closures
  capturing entry.phone directly
- Add widget tests for ContactPhoneTileAdapter covering all enable flags,
  callback wiring, transfer logic, and popup menu entries
- Add direct tests for displayPhoneEntries covering displayLabel,
  displayFavorite, and phone field contracts

* feat: melos exported env support (#993)

* fix: log records file stability improvements (#998)

* fix: guard File.delete() with exists() check in shareLogRecords

Prevents PathNotFoundException on Android 15 when the OS clears the
app cache directory under background memory pressure while the native
share sheet is open, causing the finally block to attempt deleting a
file that no longer exists (Crashlytics issue 25d22f4de9016c556b46cfacea29a20b).

* fix: delegate dispose Future in LogRecordsFileRepositoryImpl

Without awaiting or returning the Future from RotatingFileAppender.dispose(),
the file handle could remain unclosed after the caller awaited repository
disposal. Use direct return to delegate the Future without an async wrapper.

* fix: replace existsSync retry with async exists() in _getAllLogFilesWithRetry

existsSync() reads from the OS filesystem cache and can return false
immediately after forceFlush() even though the file is on disk.
Switching to async File.exists() forces a fresh stat() call that
bypasses the cache. Also reduces max wait from 10s (5x2s) to 1s (10x100ms).

* fix: guard forceFlush() with try/catch in readAllLogs

An I/O error during forceFlush() would propagate and abort readAllLogs
entirely, returning no logs to the caller. Catching the error allows
reading whatever files are already on disk.

* fix: guard forceFlush() with try/catch in cleanLogs

An I/O error during forceFlush() would abort cleanLogs entirely,
leaving stale log files on disk. Catching the error allows deletion
to proceed on whatever files are already present.

* fix: replace retry loop with async file discovery in log records

- Replace _getAllLogFilesWithRetry (10×100ms polling loop) with
  _getAllLogFilesAsync — single async pass using await file.exists()
  per rotation slot, bypassing OS filesystem cache without any delay
- Extend file discovery range to 0..keepRotateCount inclusive so
  rotated files (e.g. app_logs.log.1) are found even when base file
  is absent immediately after rotation
- Fix cleanLogs to use await _getAllLogFilesAsync() instead of
  synchronous getAllLogFiles() which relied on existsSync()
- Fix readAllLogs iteration order: remove files.reversed so newer
  file (rotation 0) is read before older rotated file (rotation 1)
- Remove redundant file.exists() guard inside readAllLogs loop since
  _getAllLogFilesAsync already guarantees file presence
- Add full test coverage: LogRecordsMemoryRepositoryImpl,
  ReadableRotatingFileAppender (readAllLogs + cleanLogs),
  LogRecordsFileRepositoryImpl — 26 tests total

* fix: update outdated comment in readAllLogs test

* feat: add Claude Code PostToolUse hooks (dart formatter + newline enforcer) (#995)

* fix: call dropped when user taps before app fully initializes (#997)

* fix: wait for signaling and registration before failing outgoing call

In __onCallPerformEventStarted, the early registration guard ran before
the signaling wait, causing calls to drop immediately when the user tapped
call before the socket initialized. Move the guard after the signaling wait
and extend the wait predicate to also require registration status to be
known (isHandshakeEstablished && isSignalingEstablished).

* fix: hold outgoing call as pending until routing state is available

When the user taps call before CallRoutingCubit has initialized (app just
launched, user info not yet fetched), wait for the first non-null routing
state instead of immediately failing. The call proceeds automatically once
routing state becomes available. If the cubit is disposed while waiting,
the call is silently dropped.

* fix: remove unused notification import from CallController

* test: add CallController.createCall unit tests

Cover immediate dispatch, pending wait, cubit disposal, and
CallUndefinedLineNotification scenarios.

* fix: remove unused optional params in _FakeCallRoutingState

* fix: remove unnecessary call_controller.dart import in test

* fix: address Copilot review — unawaited createCall and fast-fail on signaling failure

- Make createCall void by delegating to private _createCallAsync via unawaited, avoiding unawaited_futures lint at all call sites
- Add isFailure condition to signaling firstWhere predicate so outgoing calls fail immediately on signaling failure instead of waiting full timeout

* fix: update call_controller tests to use void createCall

Replace await controller.createCall(...) with call + await Future.delayed(Duration.zero)
to pump microtasks after createCall became void

* refactor: extract signaling wait predicate into named variables

* fix: use currentState instead of state for registration check after signaling wait

* fix: replace non-ASCII em dash with semicolon in comment

* refactor: remove redundant signalingConnected/registrationKnown vars, use CallState getters directly

* fix: await all addTrack calls before createOffer to prevent empty offer

* refactor: add TODO to provide CallController as singleton via RepositoryProvider

* fix: log warning when callRoutingCubit closes before routing state arrives

* refactor: add comment explaining callRoutingState await logic

* fix: add timeout to routing state wait and show NoInternetConnectionNotification on expiry

* refactor: extract _waitForRoutingState helper to clean up routing state await

* fix: catch unexpected errors from _createCallAsync and add timeout notification test

* fix: await reportNewIncomingCall in background FCM handler (#1001)

Without await the Pigeon IPC Future was fire-and-forget — the background
isolate was destroyed before the call reached the Kotlin side, so
PhoneConnectionService.startIncomingCall() was never invoked and the
incoming call UI never appeared.

* feat: provide CallController as singleton via RepositoryProvider in MainShell (#1000)

* feat: provide CallController as singleton via RepositoryProvider in MainShell

Register CallController once in MainShell widget tree after CallBloc,
CallRoutingCubit and NotificationsBloc are available. All seven call
sites now obtain the shared instance via late final field initializer
(context.read<CallController>()) instead of constructing a new object
per StatefulWidget.

* feat: replace RepositoryProvider with CallControllerScope InheritedWidget

RepositoryProvider is semantically for the data layer; CallController
is a UI-tier coordinator. Introduce CallControllerScope (InheritedWidget)
following the PresenceViewParams pattern already in the codebase.
All seven consumers now resolve the controller via CallControllerScope.of(context).

* fix: prevent stale CallController on MainShell rebuild

Store CallController in _MainShellState via ??= so it is created once
regardless of how many times build() reruns. Switch CallControllerScope.of
to getElementForInheritedWidgetOfExactType to avoid registering a rebuild
dependency that would never fire (updateShouldNotify is always false).

* feat: add doc comment for _callController field in _MainShellState

* fix: feature access mapper logic (#1007)

* fix: correct icon and text case assertions in integration tests (#1010)

* fix: correct icon and text case assertions in integration tests

* revert: remove accidentally committed logger from call_bloc.dart

* feat: add integration tests for webtrit_signaling keepalive and disconnect (#1009)

* feat: add integration tests for webtrit_signaling keepalive and disconnect

Add mock-based and live integration tests covering:
- Keepalive timeout (WebtritSignalingKeepaliveTransactionTimeoutException)
- Normal keepalive cycles (echo → timer restarts)
- Graceful disconnect (onDisconnect callback)
- Stream error (onError callback)
- Execute after disconnect (WebtritSignalingDisconnectedException)
- Request transaction timeout (non-keepalive variant)
- Live tests against a real server (skipped when credentials not set)
- Network simulation via pure-Dart WebSocket proxy with pause/resume
  to verify keepalive timeout fires on real packet drop

* feat: replace WebSocket proxy with raw TCP proxy in live signaling test

Drop packets at the TCP byte level instead of WebSocket frame level
for more realistic network simulation. The proxy rewrites the HTTP
Host header before forwarding so the server accepts the upgrade request.

* feat: extract TcpProxy into reusable _tcp_proxy package

Move the raw TCP proxy from the signaling test into a standalone
internal package packages/_tcp_proxy so it can be used as a
dev dependency in both webtrit_signaling tests and app-level tests.

* fix: address Copilot review comments on signaling tests and tcp proxy

- Fix expectLater() to pass Future directly instead of closure
- Make live test client nullable to prevent LateInitializationError in tearDown
- Use local signalingClient variable in each live test for clarity
- Replace localhost with 127.0.0.1 to avoid IPv6-first resolution issues
- Remove unconditional TLS certificate bypass in live test HTTP client
- Discard bytes while paused in _relayWithHostRewrite to prevent unbounded buffer growth
- Add 64 KB header size guard with socket teardown on overflow
- Add AGENTS.md, README.md, analysis_options.yaml to _tcp_proxy package

* fix: add lints dev_dependency to _tcp_proxy so analysis_options.yaml resolves

* fix: replace magic timing numbers with constants in integration tests

* docs: add DartDoc to _findCrlfCrlf explaining HTTP header boundary detection

* fix(signaling): reconnect silently on server force-close (code 4441) (#1012)

When the server sends disconnect code 4441 (controllerForceAttachClose)
it means two signaling sessions from the same account were open at the
same time — a race that can happen when a background push isolate is
still connected as the main engine comes to the foreground and
reconnects signaling.

Previously this code path set lastSignalingDisconnectCode, which
triggered CallStatus.connectIssue and showed a "Connection issue"
snackbar to the user even though the situation was transient and
self-healing.

Fix: on 4441, emit lastSignalingDisconnectCode=null (no connectIssue
UI), skip the notification, and reconnect with the fast 1s delay
instead of the default 3s. A warning log is emitted so the race remains
visible in logs for debugging.

* refactor: decouple AppLogger from LogzioLoggingService via RemoteLoggingService abstraction (#1011)

* refactor: decouple AppLogger from LogzioLoggingService via RemoteLoggingService abstraction

* refactor: move remoteMinLevel extraction to init to avoid cast in applyConfig

* refactor: add minLevel to RemoteLoggingService and remove cast from AppLogger

* refactor: reorder LogzioLoggingService fields to match RemoteLoggingService interface order

* refactor: remove labelsProvider from AppLogger state, pass labels explicitly

* refactor: rename regenerateRemoteLabels to updateRemoteLabels and dispose before reinitialize

* refactor: replace labels map with lazy callback in AppLogger to encapsulate labels retrieval

* fix: attach remote appender before applyConfig so early logs are forwarded to remote

* feat: enable and disable log anonymization with env (#1004)

* feat: enable and disable log anonymization with env

Added functionality to disable and enable log anonymization via environment variable

* refactor: simplify AnonymizationType to none/full enum with bool flag

Replace the mutable list-based anonymization approach with a single
AnonymizationType enum (none/full), removing the race condition risk
and simplifying the API surface.

* docs: document intentional anonymization scope in AppLogger

* fix: add @override annotation to setAnonymizationEnabled in LogzioLoggingService

---------

Co-authored-by: Dmytro Serdun <d.serdun@webtrit.com>

* fix: api healthcheck path (#1014)

* fix: use shared static logger in WebtritSignalingClient (#1013)

* fix: use shared static logger in WebtritSignalingClient with instance id prefix

* fix: restore custom logger support via inner constructor parameter

* fix: simplify logger to use fixed name without instance counter

* fix: remove custom logger parameter from inner constructor

* fix: signaling errors  (#1016)

* fix: snackbar duration

* fix: new persist option usage

* fix: reconnect message repeating

* fix: reconnect on keepalive timeout

* feat: user info cache (#1015)

* feat: main impl

* refactor: call to actions fix

* fix: call to actions provider

* docs: getAndListed explain

* fix: integration tests rescue (#1025)

* fix: migrate to 4.5

* fix: various fixes

* fix: remove log file from standart test

* fix: no audio if lone codec (#1028)

* fix: WebRTC signaling state guards — ICE restart, renegotiation, and glare (WT-986) (#1003)

* refactor: extract RenegotiationHandler with stable-state and concurrency guards

- Extract renegotiation logic from CallBloc into a standalone RenegotiationHandler class
- Add two stable-state guards: pre-offer check and TOCTOU guard after createOffer
- Add _isHandling/_pendingRetry flags to serialize concurrent onRenegotiationNeeded firings
- Catch WebtritSignalingErrorException for server-side error logging without swallowing
- Catch plain String errors (flutter_webrtc native) separately from Dart exceptions
- Add unit tests covering stable-state skip, concurrency serialization, and error paths
- Document server-mediated vs P2P topology constraints and Perfect Negotiation limitation

* fix: add signalingState guards and Perfect Negotiation rollback to call flow

ICE restart handler:
- Skip setLocalDescription when signalingState != stable to prevent native crash
- Log warning instead of silently skipping

Renegotiation / accepted handler:
- Guard setRemoteDescription(answer) against wrong state after glare resolution
- Log transceivers after setRemoteDescription for SDP debugging
- Catch String errors from setRemoteDescription to prevent unhandled exception escalation

Updating handler (Perfect Negotiation rollback):
- Pre-check signalingState for glare: if have-local-offer, roll back local offer before setRemoteDescription
- Catch String errors containing have-local-offer as fallback for stale flutter_webrtc signalingState cache
- Roll back and retry setRemoteDescription on confirmed glare

Renderer and state:
- Always refresh srcObject in RTCStreamView.didUpdateWidget to handle renegotiation-replaced tracks
- Fix remoteVideo getter to use logical OR (stream tracks || video flag) instead of short-circuit

Add call_state_test.dart covering ActiveCall equality and remoteVideo edge cases

* refactor: replace on String catch with typed RTC exceptions via RtcJsepErrorParser

* fix: guard RTCVideoRenderer srcObject assignment until initialize() completes (#1027)

* fix: guard RTCVideoRenderer srcObject assignment until initialize() completes

Prevents a crash where didUpdateWidget set srcObject before the renderer
was initialized, causing 'Call initialize before setting the stream'.

The _initialized flag ensures srcObject is only set after initialize()
resolves. Also always refreshes srcObject (not just on stream identity
change) to handle track replacement during renegotiation.

* fix: use setState and guard build before renderer initialization

Wrap _initialized and srcObject assignment in setState so the flag change
is properly synchronized with Flutter's build cycle. Guard build() to
avoid passing an uninitialized renderer to RTCVideoView — the
placeholderBuilder (or empty SizedBox) is shown until initialize()
completes.

* fix: update ExternalContactsSyncBloc tests to mock getAndListen instead of getLocalInfo (#1029)

* feat: handle 'voicemail_not_configured' error (#1005)

* feat: handle 'voicemail_not_configured' error

Added handling 'voicemail_not_configured' error from  voicemail api

* fix: properly handle voicemail_not_configured and endpoint_not_supported errors across all layers

- WebtritApiClient: skip SEVERE log for expected VoicemailNotConfiguredException / EndpointNotSupportedException
- VoicemailRepositoryImpl: add _featureSupported flag to skip API calls once feature is known unavailable; add _fetchingCompleter.future.ignore() to prevent unhandled future errors; suppress stack trace in warning log for expected exceptions; expose isFeatureSupported getter
- VoicemailRepository: add isFeatureSupported to abstract interface; EmptyVoicemailRepository returns false
- VoicemailCubit: check isFeatureSupported on init to immediately emit featureNotSupported without an API call
- SettingsBloc: catch expected exceptions from unawaited fetchVoicemails() to prevent runZonedGuarded propagation
- PollingService / ConnectivityLifecycleService: remove stack trace from WARNING logs in generic catch blocks

* fix: always allow navigation to voicemail screen regardless of feature support

Previously the settings tile was disabled with reduced opacity when voicemail
was not configured, which was confusing. Now the tile is always tappable and
the voicemail screen shows a placeholder explaining the reason.

* fix: move VoicemailCubit provider from SettingsScreenPage to VoicemailScreenPage

VoicemailCubit was provided at the settings level but consumed in a separate
route, causing ProviderNotFoundException on navigation. Moving it to
VoicemailScreenPage makes the provider scope match the consumer.

* fix: add missing call feature import to VoicemailScreenPage

* fix: remove unused stack trace variables from catch blocks in polling and connectivity services

* fix: address Copilot review comments

- VoicemailCubit: guard watchVoicemails subscription against overwriting featureNotSupported state
- VoicemailNotConfiguredException: pass token and error fields to super; remove commented-out placeholder
- WebtritApiClient: log xRequestId (never null) instead of requestId parameter; pass token/error to VoicemailNotConfiguredException
- SettingsTile: add assert that opacity is in [0.0, 1.0]

---------

Co-authored-by: Dmytro Serdun <d.serdun@webtrit.com>

* feat: add toJson() to all Event subclasses and fix NotifyEvent constructor inconsistency (#1030)

* fix: suppress transient network error SnackBar in RegisterStatusCubit (#1031)

Auto-triggered fetches (startup and connectivity restore) now use
_fetchStatusSilently(), which skips handleError for SocketException,
TimeoutException, and TlsException. The cubit retries automatically
on the next connectivity change, so surfacing these transient failures
to the user is misleading. User-initiated fetchStatus() retains the
original behaviour and still calls handleError for all errors.

* fix: add 10s timeout to reportNewIncomingCall in background message handler (WT-1061) (#1032)

Telecom on affected devices can become overloaded with phantom PhoneAccount
registrations, causing reportNewIncomingCall() to hang indefinitely. This
keeps FlutterFirebaseMessagingBackgroundService alive and blocks subsequent
cold starts. Adding a 10s timeout bounds the handler lifetime and logs a
warning when Telecom is slow to respond.

* fix: add 5s timeout to getInitialMessage() to prevent splash freeze (WT-1061) (#1033)

* refactor: remove tryUse from AppDatabaseScope, migrate callers to useOrNull (#1034)

* fix: eliminate write-write SQLite contention via shared DriftIsolate server (WT-1061) (#1035)

* fix: eliminate write-write SQLite contention via shared DriftIsolate server (WT-1061)

Spawns a single dedicated DriftIsolate server in the main isolate bootstrap
and registers its SendPort in IsolateNameServer under a fixed key.

Background isolates (FCM handler, WorkManager) now connect to the same server
via IsolateDatabase.connectOrCreate(), which looks up the port and creates a
client connection — falling back to a direct NativeDatabase connection when the
main app is not running (cold push with no foreground app).

All writes are serialized through the single server isolate, making
write-write SQLITE_BUSY (code=5) between concurrent isolates impossible.

Changes:
- app_database: add createAppDatabaseNative() for synchronous NativeDatabase
  creation inside the server isolate (no createInBackground needed)
- IsolateDatabase: add spawnServer(), connectOrCreate(), kDbPortName
- AppDatabaseScope.use(): connect via connectOrCreate() instead of create()
- bootstrap(): spawn DriftIsolate server, register DriftIsolate in InstanceRegistry
- AppDatabaseLifecycleHolder: connect to DriftIsolate, shutdown server on dispose

* fix: address Copilot review — robust spawnServer error handling and stale port cleanup (WT-1061)

* test: add integration tests for IsolateDatabase stale port handling (WT-1061)

* refactor: introduce SignalingModule stream abstraction (phase 1) (#1024)

* refactor: introduce SignalingModule stream abstraction (phase 1)

Replace SignalingManager callback-based API with SignalingModule — a
sealed-event broadcast stream that owns the WebtritSignalingClient
lifecycle without any BLoC, CallState, or UI dependency.

Key changes:
- Add SignalingModule with fire-and-forget connect(), disconnect(),
  dispose() and a sealed SignalingModuleEvent hierarchy
  (Connecting, Connected, ConnectionFailed, Disconnecting,
  Disconnected, HandshakeReceived, ProtocolEvent)
- Add isRepeated deduplication on ConnectionFailed to suppress
  repeated identical error notifications
- Map disconnect codes to recommendedReconnectDelay:
  4441 → Duration.zero, protocolError → null, all others → 3 s
- Migrate CallBloc from direct WebtritSignalingClient callbacks to
  SignalingModule stream subscription; new _SignalingClientEvent
  variants: connecting, connected, failed, disconnecting, disconnected
- Migrate IsolateManager (Push + Foreground) to SignalingModule,
  replacing SignalingManager; add connectivity monitoring and pending
  request queue inside IsolateManager
- Construct SignalingModule in main_shell.dart and inject into CallBloc
- Delete SignalingManager and remove its export from common.dart
- Add 31 unit and integration tests for SignalingModule

* fix(test): update ExternalContactsSyncBloc tests for getAndListen API

The BLoC was updated to call userRepository.getAndListen() instead of
getLocalInfo(), but the mocks were never updated. Fix the setUp mock
and correct the RefreshFailure test to use load() failure (which is
the actual trigger for that state) rather than userRepository failure.

* fix(test): rename local function to avoid leading underscore lint warning

* docs: translate signaling architecture doc to English

* docs: remove phase 1 requirements planning doc from repo

* refactor: remove coreUrl/tenantId/token/trustedCertificates from CallBloc

These four fields were passed through CallBloc only to construct
SignalingModule internally. Now that SignalingModule is constructed
externally and injected via the constructor, the fields are dead code.
Remove them and the corresponding import from ssl_certificates.

* fix: address Copilot review comments on SignalingModule/IsolateManager

- Guard delayed reconnect callbacks with signalingClient == null check to
  avoid tearing down a healthy connection that connected during the delay
- Populate _incomingCallEvents from handshake and protocol events so
  _findIncomingEventLog returns real caller data instead of null
- Use disconnect() instead of dispose() in handleLifecycleStatus so the
  module remains reusable when the app returns to the foreground
- Fix post-dispose connect() test to actually subscribe to the event stream
  and assert Connecting/Connected events are absent after dispose

* feat: replay session events to late subscribers in SignalingModule

Adds a per-subscriber replay buffer so that consumers created after
connect() (e.g. CallBloc constructed after SignalingModule already
connected and received a handshake) do not miss any events from the
current session.

- events getter now returns a single-subscription stream that first
  replays all events buffered since the last connect() call, then
  pipes live events from the broadcast controller
- connect() clears the buffer so late subscribers see only the
  current session, not stale events from previous reconnect cycles
- dispose() also clears the buffer on teardown
- Uses sync: true on the intermediate StreamController to avoid an
  extra async hop and keep delivery ordering consistent with callers
  that await module operations
- Adds two integration tests covering the late-subscriber replay
  and the buffer-clear-on-reconnect behaviours

* feat: connect SignalingModule early in initState to reduce call setup latency

SignalingModule is now created and connected in _MainShellState.initState(),
running the WebSocket handshake in parallel while the widget tree and
CallBloc are being built. When CallBloc is eventually created it subscribes
to the replay stream and receives all buffered session events without missing
anything.

_MainShellState.dispose() owns the module lifecycle; CallBloc.close()
still calls dispose() on the module (idempotent, safe).

* docs: update signaling architecture doc with layer descriptions and diagrams

* fix: add concurrency lock to _connectAsync to prevent parallel connects

* fix: await disconnect ack in dispose() to prevent SignalingDisconnected drop on race

* fix: suppress reconnect hint on intentional disconnect to prevent spurious reconnect

* fix: remove SignalingModule.dispose() from CallBloc.close() — ownership belongs to MainShellState

* fix: snapshot buffer before live subscribe to prevent replay duplicates in events getter

* fix: store reconnect Timer in IsolateManager so it can be cancelled on close()

* fix: forward recommendedReconnectDelay from SignalingDisconnected to _scheduleReconnect in CallBloc

* fix: suppress _onDisconnect after _onError to prevent double reconnect scheduling

* fix: exclude SignalingProtocolEvent from session buffer to prevent unbounded growth

* fix: replace force-unwrap of session.coreUrl/token with null-safe logout in initState

* fix: remove performEndCall early return so pre-handshake declines are queued

* fix: close liveController on subscription cancel to prevent StreamController leak

* fix: use _networkNone state instead of stale results snapshot in connectivity timer closure

* docs: fix _scheduleReconnectIfNeeded → _scheduleReconnect in signaling architecture doc

* test: replace Future.delayed(Duration.zero) with pumpEventQueue() in signaling module tests

* fix: make _controller sync:true to eliminate async-dispatch event duplication

* docs: clarify disconnect() docstring — SignalingDisconnected is callback-driven, not synchronous

* fix: wrap _signalingModule.disconnect() in unawaited() in _disconnectInitiated

* fix: remove unused shouldReconnect variable from __onSignalingClientEventDisconnected

* test: add 8 tests to reach 100% SignalingModule coverage

- concurrent connect() dropped while factory in-flight (_connecting guard)
- intentional disconnect() emits SignalingDisconnected with null delay
- disconnect() passes goingAway code to the underlying client
- _onError suppresses subsequent _onDisconnect (_errorHandled flag)
- SignalingProtocolEvent excluded from replay buffer
- cancelled subscription receives no further events
- dispose() awaits disconnect ack before closing the stream
- _onHandshake/_onEvent are no-ops after dispose()

* test: add scenario-driven SignalingModule tests from CallBloc usage analysis

Covers scenarios observed in CallBloc's signaling subscription:

internet dropped mid-session:
- _onError after handshake → ConnectionFailed not Disconnected, signalingClient cleared
- Unexpected socket close (null code) → Disconnected with kSignalingClientReconnectDelay
- ConnectionFailed buffered so late subscribers reconstruct last-known failure

handshake not completed:
- Disconnect before handshake → no HandshakeReceived in buffer, Disconnected with delay
- Late subscriber after no-handshake disconnect → gets Connecting+Connected+Disconnected only
- Error before handshake → ConnectionFailed buffered, no HandshakeReceived
- Reconnect after no-handshake failure delivers fresh session events

late subscriber mid-session:
- Factory still pending → gets Connecting from buffer, Connected arrives live
- After full connect+handshake → all three lifecycle events replayed, no protocol events

disconnect() robustness:
- client.disconnect() throws → dispose() still completes without hanging
- Second disconnect() with no active client is a silent no-op

* fix: restore callkeep_signaling_status_converter.dart lost during rebase

* fix: remove unused fields in _ThrowingDisconnectClient test helper

* fix: use normalClosure (1000) instead of goingAway (1001) when client disconnects WebSocket

* test: update disconnect test to expect normalClosure (1000) instead of goingAway (1001)

* fix: address Copilot review comments in SignalingModule and IsolateManager

- Fix doc comment on events getter: protocol events are not replayed, only lifecycle/handshake events
- Guard connect() buffer clear behind _connecting check to avoid clearing on redundant calls
- Remove stale comment in _onDisconnect that contradicted the !_disposed guard
- Treat empty connectivity result as offline in _monitorConnectivity (results.isEmpty || any(none))
- Treat empty connectivity result as offline in performAnswerCall (isNotEmpty && !contains(none))

* fix: address post-review issues in SignalingModule and IsolateManager

- isolate_manager: fix connectivityNoneCounter reset — error now fires
  exactly once at maxConnectivityNoneRepeats; subsequent none-events are
  silently ignored until connectivity is restored and counter resets to 0

- main_shell: split SignalingModule construction into valid/invalid-creds
  branches to remove ?? '' fallbacks and make intent explicit

- signaling_module: document sync:true reentrancy assumption, single-use
  constraint on events getter, and _errorHandled ordering invariant

* revert: restore main_shell.dart SignalingModule construction with ?? '' fallback

The split-branch approach still created a module with empty strings in the
null-creds case — identical in behaviour to the original. Reverted to the
original form which is honest about the fallback until a proper nullable
refactor is done.

* fix: remove dead null-guard in MainShellState.initState for SignalingModule

The router guard (onMainShellRouteGuardNavigation) redirects to login
when state.status != authenticated, so coreUrl and token are always
non-null when MainShell is mounted. Replace ?? '' fallbacks and the
unreachable null-branch with direct ! unwraps.

* docs: sync signaling_architecture_target.md and call_architecture.md with current code

- signaling_architecture_target: add timer cancellation to IsolateManager
  and CallBloc _scheduleReconnect snippets (Future.delayed → Timer with cancel)
- signaling_architecture_target: add missing SignalingDisconnecting case to
  CallBloc subscription snippet
- call_architecture: update ownership — CallBloc owns SignalingModule, not
  WebtritSignalingClient directly

* fix: replace non-ASCII characters with ASCII equivalents in Dart sources

Replace em dash (U+2014) with '-' and right arrow (U+2192) with '->'
in comments and test descriptions across signaling_module.dart,
isolate_manager.dart, signaling_module_test.dart, and call_bloc.dart.

* fix: incorrect styling of status bar on app start (#1006)

Fixed status bar rendering with incorrect styling on initial app launch when theme is light

* feat: show progress indicator while sharing logs (#1036)

* feat: show progress indicator while sharing logs

Co-Authored-By: Dmytro Serdun <serdun@webtrit.com>

* refactor: replace inline SizedBox+CircularProgressIndicator with SizedCircularProgressIndicator

---------

Co-authored-by: Dmytro Serdun <serdun@webtrit.com>

* fix: upgrade to video resets hold (#1038)

* fix: media settings parsing (#1039)

* fix: call drops after theme or lang change (#1041)

* fix: cannot make calls after blind transfer — skip hangup + reconnect safety net (WT-1214) (#1040)

* fix: trigger reconnect when starting outgoing call with no signaling (WT-1214)

After a blind transfer, the signaling WebSocket is closed with code 4610
and the disconnect is marked intentional by SignalingModule, so no
reconnect is scheduled. Subsequent outgoing call attempts enter
outgoingConnectingToSignaling and wait passively — neither signalingReady
nor signalingFailed ever fires, causing the call to fail on timeout.

Add _scheduleReconnect(Duration.zero) at the start of the waiting block
so that initiating an outgoing call always recovers the signaling
connection, regardless of whether the previous disconnect was intentional.

* fix: skip hangup after successful blind transfer to avoid 4610 disconnect (WT-1214)

When a blind transfer completes (NOTIFY SIP/2.0 200 OK +
subscription_state: terminated), the SIP dialog is already closed
server-side via REFER. Sending a hangup request on the freed dialog
causes the server to close the WebSocket with code 4610 ("call request
on wrong line error"), triggering an unintended signaling disconnect.

Check the call's transfer state: if it is Transfering(fromBlindTransfer:
true), skip the hangup request and clean up the peer connection locally
only. This removes the root cause of the 4610 disconnect that led to
signaling not being reconnected for subsequent outgoing calls.

* test: cover blind-transfer hangup skip and 4610 reconnect hint

Add tests for two fixes from WT-1214:

- Transfer — isBlindTransferCompleted detection (call_state_test.dart):
  verifies the switch pattern that decides whether to skip the hangup
  request after a blind transfer. Covers Transfering(fromBlindTransfer:
  true/false), earlier transfer states, and null.

- SignalingModule — requestCallIdError (4610) reconnect hint
  (signaling_module_test.dart): verifies that a non-intentional 4610
  carries a non-null recommendedReconnectDelay (reconnect scheduled),
  while an intentional disconnect() followed by a server 4610 emits
  null (reconnect suppressed — the scenario that triggered WT-1214).

* refactor: rename isBlindTransferCompleted → isBlindTransferInTransferingState

Transfering state in the model means "server started to process the
transfer", not "transfer completed". Rename the local variable and update
the surrounding comments and test group names to match the actual Transfer
model semantics, avoiding misinterpretation of the hangup guard.

Addresses Copilot review comments on PR #1040.

* fix: call or transfer to myself handling (#1046)

* fix: hide video for held call (#1048)

* fix: hide video for held call

* fix: tap area

* fix: transfer to same recipient (#1049)

* refactor: extract SignalingModuleInterface; migrate CallBloc and IsolateManager; extract toLinesState (#1045)

* refactor: extract SignalingModuleInterface; decouple IsolateManager from SignalingModule

Add local SignalingModuleInterface abstract class to signaling_module.dart
with the contract needed by IsolateManager: events, isConnected, connect(),
disconnect(), execute(Request), dispose().

SignalingModule implements the interface, gaining isConnected and execute()
alongside the existing signalingClient getter (kept for backward compat).

IsolateManager field type changed from SignalingModule to SignalingModuleInterface.
All signalingClient null-checks replaced with isConnected; direct client.execute()
calls replaced with module.execute(). IsolateManager no longer depends on the
concrete class, making it ready for a plugin-backed implementation.

* fix: decouple NetworkCubit from WebtritSignalingService; fix push dedup race

NetworkCubit held a concrete WebtritSignalingService only to call
updateMode(). Replaced with a Future<void> Function(SignalingServiceMode)
callback so the cubit has no plugin dependency and is mock-testable.
Call site passes WebtritSignalingService().updateMode as a tear-off.

_onCallPushEventIncoming checked the incomingFromOffer guard before
awaiting contactNameResolver.resolveWithNumber(). The signaling path
could create an ActiveCall during that async gap, causing both paths
to emit separate entries for the same callId. Added a post-await guard
that checks for any existing ActiveCall with the same callId before emitting.

* fix: prevent premature call routing state emission before signaling handshake (#1044)

LinesState.blank() is emitted at app startup before the signaling handshake
arrives. combineLatest fires immediately with cached UserInfo + blank LinesState,
causing CallRoutingCubit to emit a non-null state with empty mainLines.
CallController then skips _waitForRoutingState() and fails with
"no idle lines available".

Fix: add LinesState.isBlank discriminator (guestLine == null is an unambiguous
pre-handshake marker — CallBloc always sets guestLine to non-null after any
handshake). Return null from _combineInfo when linesState.isBlank so the cubit
stays in its unready state and CallController waits correctly.

* fix: preserve LinesState.blank in onChange until signaling handshake arrives

The previous fix relied on guestLine == null as a pre-handshake discriminator,
but CallBloc.onChange always set guestLine = LineState.idle regardless of
linesCount, overwriting LinesState.blank() almost immediately after startup.

Root cause: onChange fired with linesCount = 0 on any early state change
(e.g. connecting status) and produced LinesState([], LineState.idle).
isBlank returned false, so _combineInfo emitted a non-null CallRoutingState
with empty mainLines, and hasIdleMainLine = false blocked the call.

Fix: when linesCount == 0 (handshake not yet received), onChange now stores
LinesState.blank() explicitly. Once the handshake sets linesCount > 0,
normal LinesState with non-null guestLine is produced as before.

* fix: remove unused models import from NetworkScreenPage

* fix: wire BackgroundSignalingBootstrapService into NetworkCubit callback

* refactor: migrate CallBloc to SignalingModuleInterface

Apply the same interface migration already done for IsolateManager:
- change _signalingModule field/param type from SignalingModule to SignalingModuleInterface
- replace signalingClient?.execute() with execute() from the interface
- replace signalingClient != null checks with isConnected

* fix: address Copilot review comments on signaling module interface

- Handle null execute() in _executePendingRequests: complete completer
  with error and clean up instead of leaving request to time out silently
- Handle null execute() in _sendRequest: log warning and return early
  instead of awaiting null when module disconnects after isConnected check
- Fix linesCount == 0 guard: use isHandshakeEstablished to distinguish
  pre-handshake blank state from valid post-handshake 0-lines state
- Update LinesState.isBlank doc comment to remove inaccurate claim that
  guestLine == null is an unambiguous pre-handshake marker

* refactor: extract toLinesState() from CallBloc.onChange into CallState

Pure deterministic logic moved to CallState.toLinesState() so it can be
tested without standing up CallBloc. onChange becomes a single line.

Tests cover: pre-handshake blank, 0-lines post-handshake with guest line,
main line idle/inUse combinations, guest line inUse alongside main calls.

* fix: rename _kRegistered to kRegistered (lint: no_leading_underscores_for_local)

* refactor: rename SignalingModuleInterface → SignalingModule, impl → SignalingModuleIsolateImpl

Drop the `Interface` postfix from the abstract contract and suffix the
concrete WebSocket implementation with `IsolateImpl` to clarify its role.
Add doc comments to every member of the SignalingModule interface.

Files touched:
- lib/features/call/services/signaling_module.dart
- lib/features/call/services/isolate_manager.dart
- lib/features/call/bloc/call_bloc.dart
- lib/app/router/main_shell.dart

* fix: update signaling_module_test to use SignalingModuleIsolateImpl concrete type

* fix: use SignalingModule.execute instead of signalingClient getter (#1050)

signalingClient is not part of the SignalingModule interface — only
SignalingModuleIsolateImpl exposes it. Replace with execute() which is
defined on the interface and returns null when not connected.

* fix: skip decline when push-registered call receives signaling line (#1051)

* fix: skip decline when push-registered call receives signaling line (WT-1091)

When an incoming call arrives via FCM push, it is registered with
line _kUndefinedLine (-1) as a placeholder. When the signaling
WebSocket subsequently delivers the same call with a real line (e.g. 0),
the guard that detects 'call to myself' incorrectly fires because
-1 != 0, causing a DECLINE to be sent on the wrong line and the call
to be dropped with server error 4610.

Fix: exclude _kUndefinedLine from the line-mismatch check so that
push-placeholder calls are updated with the real line rather than
declined.

* test: add push → signaling line handoff tests for WT-1091 fix

* feat: media settings ptime warning (#1053)

* fix: call glare (#1052)

* fix: call glare

* fix: commented code

* feat: call interaction guard if any updating (#1056)

* fix: stop ringback sound on forced call termination (#1055)

__onResetStateEventCompleteCall is the path taken when signaling disconnects
unexpectedly (network loss). Unlike other termination paths it was not calling
_stopRingbackSound(), leaving the ringback tone playing after the call UI disappeared.

* fix: sanitize keypad input before initiating a call (WT-1026) (#1057)

* fix: sanitize keypad input before initiating a call (WT-1026)

Apply PhoneParser.normalize() in _popNumber() so that Unicode lookalike
digits and stray formatting characters are stripped before the number
reaches CallController/CallBloc.

* fix: normalize keypad input at entry point via TextInputFormatter (WT-1026)

Replace the call-time normalize approach with a TextInputFormatter so
pasted or typed Unicode lookalike characters are sanitized immediately
in the TextField, keeping the displayed text and the dialed number
consistent.

* refactor: move PhoneNormalizingFormatter to keypad/utils (WT-1026)

Extract formatter into its own file under features/keypad/utils/ with a
barrel export, following the existing package structure convention.

* fix: strip non-dialable characters from keypad input (WT-1026)

After Unicode normalization, remove any character outside [0-9*#+] so
pasting arbitrary text (e.g. *"{) leaves only valid phone number chars
in the field.

* fix: address Copilot review comments on PhoneNormalizingFormatter (WT-1026)

- Cache _nonDialableChars as static final to avoid per-keystroke allocation
- Translate both baseOffset and extentOffset (preserving affinity/isDirectional)
  instead of always returning a collapsed selection
- Extract sanitize() static helper and reuse it in _popNumber() as a
  belt-and-suspenders guard for programmatic controller updates

* test: add unit tests for PhoneNormalizingFormatter (WT-1026)

Covers sanitize() and formatEditUpdate() — including Unicode normalization,
non-dialable character stripping, cursor translation, and selection range
preservation.

* refactor: extract renegotiation support (#1058)

* fix: prevent null crash in InkWell on hardware keyboard event during navigation (WT-1012) (#1060)

* fix: prevent null crash in InkWell on hardware keyboard event during navigation (WT-1012)

Remove debug focusColor/hoverColor from CdrTile InkWell to stop it
subscribing to _HighlightModeManager, and add FocusScope.unfocus()
before all fullscreenDialog navigations to cover ListTile-based widgets.

* fix: use FocusManager and mounted guard in openNotificationsScreen (WT-1012)

Replace FocusScope.of(context).unfocus() with
FocusManager.instance.primaryFocus?.unfocus() to avoid depending on
a potentially unmounted context, and add a mounted guard before
navigating, since the callback can fire from a stream after disposal.

* fix: voicemail audio cache collision on Android (WT-1016) (#1061)

* fix: resolve voicemail audio cache collision on Android (WT-1016)

All voicemails shared the same cache file path because the last URL
path segment is always 'attachment'. Concurrent LockCachingAudioSource
instances raced to rename the same .part file, causing a crash.

- Add cacheKey param to AudioView; VoicemailTile passes voicemail.id
- _getCacheFile falls back to joined URI segments when cacheKey is absent
- Ensure media_cache directory exists before screen initialises

* refactor: ensure media_cache dir exists in AppPath.init

Move Directory.create to AppPath.init so the path is ready to use
by the time any feature accesses mediaCacheBasePath, consistent with
how getApplicationDocumentsDirectory / getTemporaryDirectory work.

* refactor: move media_cache dir creation into AudioView._getCacheFile

AppPath is platform-agnostic and must not import dart:io (web support).
Directory is now created in _getCacheFile, co-located with the code
that writes the cache file and already guarded by the Platform.isIOS check.

* fix: address Copilot review comments on AudioView cache handling

- Move Directory.create to _initialize() as async IO, avoiding
  synchronous filesystem call in _getCacheFile on every retry
- Guard empty rawKey with early return null so just_audio handles
  caching when URI has no path segments
- Sanitize cacheKey against path separators to prevent path traversal
  from server-provided voicemail IDs

* refactor: renegotiation handlers per call (#1065)

* fix: fall back to audio-only when camera permission is denied on incoming video call (WT-1049) (#1062)

* fix: fall back to audio-only when camera permission is denied on incoming video call (WT-1049)

When answering an incoming video call without camera permission, the app
no longer drops the call. Instead, it retries media acquisition with
video disabled and answers audio-only. If the microphone itself is
unavailable the error still propagates as before.

* refactor: check camera permission before requesting media instead of relying on error fallback (WT-1049)

Added `isVideoAvailable()` to the `UserMediaBuilder` contract so the BLoC
can explicitly query camera permission status before deciding whether to
request video. Replaces the previous error-based fallback approach with a
proactive permission check.

* refactor: inject camera permission check as callback into UserMediaBuilder (WT-1049)

Replaced the direct `permission_handler` dependency in `user_media_builder.dart`
with an optional `isCameraPermissionGranted` callback. `DefaultUserMediaBuilder`
remains free of plugin dependencies; the check is wired at construction time in
`main_shell.dart` via `AppPermissions.isPermissionGranted(Permission.camera)`.

* refactor: move permission-aware video resolution into UserMediaBuilder.build() (WT-1049)

`DefaultUserMediaBuilder.build()` now resolves the effective video flag
internally via `_isCameraAvailable()` before calling `getUserMedia`, so
callers pass their intent (`video: offer.hasVideo`) and the builder handles
the permission check transparently.

`call_bloc.dart` derives the resulting video state from the actual stream
tracks (`localStream.getVideoTracks().isNotEmpty`) instead of pre-computing
it, removing all permission logic from the BLoC layer.

* fix: scope audio fallback to incoming answer only via allowAudioFallback flag (WT-1049)

The unconditional permission-check fallback inside `build()` was silently
downgrading video for all callers, including the camera-enable action which
relies on `UserMediaError` to show a notification and skip the `video: true`
state update.

Introduced `allowAudioFallback: bool = false` on `build()`. Default behaviour
(throw on failure) is preserved for existing callers; `__onCallPerformEventAnswered`
opts in with `allowAudioFallback: true` to get the permission-aware fallback.

* test: add unit tests for DefaultUserMediaBuilder permission-aware fallback (WT-1049)

* fix: retry getUserMedia audio-only when video acquisition fails with allowAudioFallback (WT-1049)

* fix: stop voicemail polling after server responds with unsup…
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants