dreamic 0.12.4 copy "dreamic: ^0.12.4" to clipboard
dreamic: ^0.12.4 copied to clipboard

discontinued
PlatformAndroidiOS

DISCONTINUED — no longer publicly maintained. A general-purpose package for Flutter/Firebase apps.

0.12.4 #

Documentation-only release — package discontinued on pub.dev. No code or behavior changes.

  • README and package description now state the package is discontinued and no longer publicly maintained; do not adopt it for new projects.
  • Removed the repository/issue_tracker pubspec links (the source repository is no longer public).
  • Previously published versions remain available as-is under the licenses they were published with.

0.12.3 #

Robustness follow-up to 0.12.2 (edge-path; nothing reported from the field). No behavior change for a correctly-bootstrapped app.

  • The six network config getters degrade to their default when Remote Config is not yet registered, instead of throwing a GetIt StateError — matching the long-standing logLevel/breadcrumbLevel fallback. A consumer that reads a network* knob before appInitRemoteConfig (or in a unit test that never registers Remote Config) now gets the default; the kill-switch specifically defaults to ON, so the resilience layer stays active on an early read. This retires the "works after Remote Config init, throws before it" failure class (the same shape as the 0.12.2 mock-RC crash).

Internal (no API or behavior change): scrubbed the private repo name from two public doc comments and extended the repo-boundary guard test to ban the bare name (the prior paths-only ban had missed it).

0.12.2 #

Fixes a crash in the mock/fake Remote Config path introduced by 0.12.1's dreamic_network_confirmation_enabled kill-switch — the first bool default in defaultRemoteConfig. RemoteConfigRepoMockImpl (registered when the backend emulator is in use or Firebase is not initialized) read defaults with bare as String/as bool casts, so the kill-switch getter's getString is-set probe threw TypeError: type 'bool' is not a subtype of type 'String', crashing network-checking initialization — no reachability probes ran, offline was never detected, and the ConnectionToaster never showed under the emulator. The live Firebase Remote Config path was unaffected (the SDK stringifies defaults), so production behavior of 0.12.1 was correct; this only broke emulator / Firebase-less runs.

  • RemoteConfigRepoMockImpl accessors now mirror the live SDK's semantics: defaults are read in string form and parsed per accessor (getString → stringified value or '' when unset; getBooltoLowerCase() == 'true'; getInt/getDoubletryParse or 0/0.0) instead of unchecked casts.
  • Regression tests pin the production mock against the real defaultRemoteConfig map (including the bool kill-switch read that crashed) and its Firebase accessor parity.

Also hardens three boundaries found in a post-release review of the 0.12.1 changes (all edge-path; none reported from the field):

  • Kill-switch mid-session flip fences. dreamic_network_confirmation_enabled is read live per status event (so it can be flipped from Remote Config without a release — its purpose). If it flipped OFF while a confirmation pass was in flight, (a) a connected verdict processed by the kill-switch-off path did not supersede the straggling pass, which could then emit a stale NetworkStatus.none over the fresher connected with nothing left to self-correct, and (b) the fast ~2s offline cadence armed before the flip was never restored to the connected interval, polling at 5× indefinitely. The kill-switch path now bumps the supersession token on connected and re-pins the fixed connected-interval cadence (pre-hotfix parity) on every event.
  • Programmatic config defaults are now clamped to configBounds. The env and Remote Config paths were already clamped, but the code path (AppConfigBase.network...Default = x) returned raw values — e.g. a networkConfirmationProbeCountDefault = 0 produced a zero-probe confirmation (flipping offline unconfirmed), and a 0 interval armed a Timer(0) busy-poll. Effective values are now clamped per-knob (in addition to the existing cross-knob validation).
  • The false-alarm resync is throw-hardened. A failure inside the post-suppression resubscribe no longer escapes as an unhandled zone error (the package's next poll self-corrects the state).

No API changes.

0.12.1 #

Network-resilience hotfix for AppCubit's online/offline handling. The whole online/offline verdict previously rested on a single fragile reachability probe with no smoothing and slow recovery, producing two user-visible symptoms even on good networks: a full-screen "No Connection" page for a few seconds on cold start, and a "Connecting..." toast that appeared on brief blips (and on resume from background) and lingered 10–25s+ while requireNetwork actions were disabled. This release makes the probe confirm-before-flip, retry on cold start, self-heal, re-probe on resume, poll adaptively, and stop leaking checkers — all tunable via the standard AppConfigBase env → Remote Config → default pattern.

Consumers that read networkStatus (or mount ConnectionToaster) inherit these behavioral changes with no code changes. The public API is additive-only (new config getters/setters + @visibleForTesting seams), so existing apps compile unchanged.

Behavior changes (surfaced explicitly, not baked in) #

  1. Confirm-before-flip before going offline. A single failed reachability probe no longer flips the app offline. NetworkStatus.none is emitted only after an immediate confirmation probe also fails, so a blip shorter than the confirmation window shows no toast and does not disable requireNetwork actions. The trade-off is a small added latency before the offline UI activates on a genuine outage — bounded by networkConfirmationProbeCount × networkProbeTimeoutMill (with the new defaults, up to ~5s; on the resume path, up to ~10s). A sustained outage still surfaces correctly.

  2. Per-probe timeout 3s → 5s. The reachability probe now allows 5s (was the package default of 3s), giving cold-DNS/TLS and marginal links headroom before a probe is counted as failed. Tunable via networkProbeTimeoutMill.

  3. Offline poll cadence 10s → 2s (adaptive). While offline, dreamic re-checks at a fast ~2s cadence (was a fixed 10s) and flips back to connected within ~2–3s of real recovery; the cadence returns to the normal ~10s interval once connected. During a sustained outage the fast cadence persists (accepted trade-off — see the tuning knob below to raise it in the field). The same ~2s cadence drives the cold-start self-heal.

  4. Probe success widened from == 200 to < 500. Any definitive HTTP response proving the path works now counts as reachable (previously only an exact 200), so legitimate non-200 responses (redirects, 204, etc.) no longer read as offline. Note: as before, a captive-portal login page can read as reachable — an accepted, pre-existing limitation.

  5. Web probe target is now resolved by dreamic (kIsWeb). When no explicit connectionCheckerUrlOverride is set, the reachability endpoint resolves to Uri.base.origin on web and https://<projectId>.web.app on mobile. For a web app served from https://<projectId>.web.app itself this is the same target as before (no change); for a web app served from any other origin (.firebaseapp.com, a custom domain, off-Firebase hosting) without the override, the old default was a cross-origin HEAD that is permanently CORS-blocked → permanent false-offline, and those apps now get a CORS-safe same-origin probe by default. An explicit connectionCheckerUrlOverride still takes precedence. Caveat: Uri.base.origin is backend-faithful only when the web build is served from the Firebase project's own origin — a web app hosted elsewhere should set the override to a backend-faithful URL.

  6. Cold-start retry, self-heal, and resume re-probe. The cold-start reachability check now retries within its budget (default ~10s) before declaring networkError, so a single slow first probe no longer produces the error page. Trade-off, surfaced explicitly: on a device that is genuinely offline at cold start, the "No Connection" page now appears only after the retry budget is exhausted (~10s with defaults, tunable via networkStartupRetryBudgetMill), where 0.12.0's single fast-failing probe often surfaced it in ~1–3s — and each Retry tap during a sustained outage re-runs the loop. This is the deliberate cost of eliminating the false-positive error page on healthy networks. If the error page does appear and connectivity is available, it self-resolves within ~2–3s without user interaction. On resume from background, an explicit lifecycle re-probe (kill-switch-gated) routes through confirm-before-flip so a cold-radio failure does not flash the offline toast — note this adds one reachability HEAD to the probe endpoint on every foreground of every consumer app. Tapping Retry no longer accumulates leaked connection checkers (old instance disposed, old subscription cancelled).

  7. internet_connection_checker_plus pinned to >=3.1.0 <3.2.0. The floor is raised (the teardown path calls dispose(), added in 3.1.0) and the minor is capped (the confirm/resync logic couples to package internals). dreamic is the sole depender on this package, so the tight pin constrains no consumer, and locks already resolve 3.1.0 — non-breaking in practice.

Additive (non-breaking) #

  • Five new AppConfigBase tuning knobs (env → Remote Config → default, RC-clamped and cross-validated), all with dreamic_network_* snake_case RC keys / dart-defines and camelCase Dart getters/...Default setters:
    • networkPollIntervalConnectedMill — RC dreamic_network_poll_interval_connected_mill, default 10000 (bounds 2000–120000)
    • networkPollIntervalOfflineMill — RC dreamic_network_poll_interval_offline_mill, default 2000 (bounds 500–30000)
    • networkProbeTimeoutMill — RC dreamic_network_probe_timeout_mill, default 5000 (bounds 1000–30000)
    • networkStartupRetryBudgetMill — RC dreamic_network_startup_retry_budget_mill, default 10000 (bounds 3000–60000)
    • networkConfirmationProbeCount — RC dreamic_network_confirmation_probe_count, default 1 (bounds 1–5)
  • Kill-switch networkConfirmationEnabled — RC / dart-define dreamic_network_confirmation_enabled, default true. When false, dreamic reproduces full pre-hotfix parity (immediate flip on the first failed probe, fixed connected-interval cadence, log-only lifecycle listener), so a field regression in the new adaptive layer can be neutralized from Remote Config without an app-store release. It is the first RC-backed bool in AppConfigBase (resolved via getBool).
  • New @visibleForTesting seams on AppCubit (e.g. probeOnceForTest, handleInternetStatusForTest), mirroring the existing handleVersionUpdateForTest pattern. Additive public symbols only.

Preserved #

  • The sticky updateRequired startup guarantees are unchanged — a too-old-version block still survives network transitions, retries, and error paths. All startup appStatus emits still route through the single startup emit path; the new confirmation/retry/adaptive-cadence logic touches only networkStatus.

0.12.0 #

FCM token-lifecycle overhaul — fixes a set of verified bugs that broke push notifications for apps on the recommended DreamicServices.initialize() wiring (no local FCM cleanup on logout, swallowed persist failures marked "synced", a cold-start race dropping silent token capture, getToken() == null latching init, and pending-payload lifecycle losing queued tokens). Under 0.x semver the minor position is the breaking slot, so this ships as 0.12.0.

Breaking changes (surfaced explicitly, not baked in) #

  1. NotificationService.initializeFcmToken now returns a success/failure signal (Future<FcmTokenInitResult>) instead of Future<void>. The new FcmTokenInitResult enum (success / captureFailed / skippedQuietly) lets callers see whether capture actually succeeded. If you awaited the old Future<void> you can keep ignoring the result, but a variable typed Future<void> assigned from it will no longer compile.

  2. Custom DeviceServiceInt implementers must add handleLoggedOut(). The interface gained a handleLoggedOut() member (resets the cached change-detection markers on logout so the next login re-syncs). This is a source break for any external implements DeviceServiceInt; add the method.

  3. The default token-changed callback(s) now RETHROW persistence failures instead of swallowing them. DreamicServices.defaultTokenChangedCallback (and the legacy NotificationService default callback) now throw on any Left/exception rather than logging-and-swallowing, so a failed persist no longer marks the token "synced" and the next start/login/trigger retries until confirmed. If you COMPOSE defaultTokenChangedCallback inside your own callback, run your non-sync side effects BEFORE awaiting it and do NOT wrap it in a swallowing try/catch (swallowing re-introduces the old mark-synced-on-failure bug for your app). The rethrown object is a dreamic-INTERNAL typed carrier that PRESERVES the failure classification (see the observability note below).

  4. NotificationInitResult gained a new value, successNoTokenSync. It is returned when permission was granted but the token could not be synced (no token callback wired, or capture/persist failed) — always alongside a loud warning. This is a Dart-3 exhaustiveness source break: an external switch over NotificationInitResult with no default/wildcard arm must add one (the same failure this release patches for its own internal/test switches). enableNotifications treats successNoTokenSync as enabled (permission WAS granted), so the app-enabled flag is not reverted on it.

  5. Status-flag semantics changed. NotificationService.isFcmTokenInitialized and NotificationSettingsDeepLinkInfo.isFcmActive now track whether a token is CURRENTLY SYNCED (last capture/persist confirmed and not since invalidated). They are NOT set on a null getToken() and are RESET on a transient refresh-persist failure, so a flag can read false while the token refresh listener is still registered and live — the prior "token acquired and listeners running" wording overstated "listeners running".

Additive (non-breaking) #

  • Web push config: FCM_WEB_VAPID_KEY / AppConfigBase.fcmWebVapidKey. Web FCM token capture now threads a VAPID key into getToken(vapidKey:). Setting a non-empty FCM_WEB_VAPID_KEY (dart-define) or AppConfigBase.fcmWebVapidKeyDefault AUTO-ENABLES web FCM (no separate USE_FCM_WEB needed) — you must also deploy a firebase-messaging-sw.js in your web app for web push to work. When web FCM is enabled but the key is empty, capture short-circuits with a quiet log rather than throwing on the missing service worker.
  • bypassCaptureBackoff optional parameter on initializeNotifications / initializeFcmToken, so explicit user/settings triggers can skip the capture backoff throttle.
  • One-time backfill heals installs already broken before this fix (server device doc missing the token while local prefs hold it) by forcing one persist, marker-stamped so it fires at most once per install.

Observability note (client-internal telemetry, not an API contract) #

The default token-changed callback rethrow now throws a dreamic-INTERNAL typed carrier (preserving the persist-failure classification) rather than a bare exception, so the distinct token-loss paths stay separable in crash reporting. This is internal telemetry, not a consumer contract — but a consumer that wraps the composed defaultTokenChangedCallback in its own try/catch would observe the carrier's runtime type.

Caveat: stale server token after OS revocation #

This release drops the deleteToken() call on the logout path and does not reconcile OS-level notification-permission revocation. A consumer whose backend does not prune not-registered tokens when a send fails may therefore retain a stale token on the server after the OS revokes permission (or after a server-side prune of a still-current token). Prune tokens on send-failure in your backend to reconcile this.

0.11.1 #

Two non-breaking fixes — a GetIt registration-order bug fix and a correction to the canonical error-reporter example. No API changes; consumers on ^0.11.0 pick this up with no constraint change.

Fix: AuthServiceInt is registered in GetIt before its initial auth-state callbacks fire #

DreamicServices.initialize previously registered AuthServiceInt in GetIt as the LAST step, after the await Future.wait(...) on the parallel device/notification init. But the AuthService's Firebase auth-state listener (attached in its constructor) fires its initial event asynchronously on that very await yield — so on a cold start with no signed-in user, the app's onLoggedOut callback ran before AuthServiceInt was registered. Any callback that resolves g<AuthServiceInt>() — directly or transitively (e.g. a repo factory that takes it as a constructor dependency) — threw Object/factory with type AuthServiceInt is not registered.

AuthServiceInt is now registered synchronously right after the AuthService is constructed (new "step 4.5"), before the first await, so it is always resolvable when the initial auth-state callbacks run. Because it now registers before the Future.wait failure point, the init-failure cleanup also unregisters it (identical-guarded, mirroring device/notification) so a gate retry re-registers a fresh, live instance rather than serving a disposed one.

Impact: apps whose onAuthenticated/onLoggedOut callbacks resolve AuthServiceInt (or a service/repo that does) during the initial cold-start auth state no longer throw. No call-site changes required.

Docs: canonical main() examples now call ensureInitialized() inside the guarded zone #

lib/error_reporting/error_reporter_example.dart (CANONICAL 1/2/3) previously showed WidgetsFlutterBinding.ensureInitialized() above DreamicErrorHandling.runGuarded(...). Because runGuarded is runZonedGuarded (a child zone), initializing the binding in the root zone while runApp runs in the child zone trips Flutter's debug "Zone mismatch" warning. The examples now call ensureInitialized() as the first line inside the guarded zone (same zone as runApp), and note the body may be () async { ... } when pre-runApp setup needs an await. Debug-only — the check is assert-gated and stripped from release/profile — but the corrected pattern removes the warning. Consumers should make the same change in their main() (move ensureInitialized() inside runGuarded).

0.11.0 #

Error-reporting hardening: capture completeness, breadcrumb gating, fail-closed redaction, multi-backend fan-out #

All changes are additive / non-breaking — existing call sites are unchanged and ErrorReporter gained only default-bodied members, not new abstract requirements. Consumers raise their dreamic constraint to ^0.11.0 and reconcile their reporter against the canonical commented example (lib/error_reporting/error_reporter_example.dart). This CHANGELOG entry is the sync mechanism — there is no automated parity test (ERH-024, ERH-025).

This release funnels every error signal (Flutter-framework, async/platform, isolate, explicit loge(), and — on web under Path A′ — JS window.onerror/unhandledrejection) through one Dart chokepoint, fans out to one or more backends, gates + redacts breadcrumbs, and makes the capture path non-throwing and re-entrancy-safe.

New API (all optional / additive)

  • DreamicErrorHandling (lib/error_reporting/dreamic_error_handling.dart) — the public, backend-agnostic capture facade a consumer main() wires up:
    • DreamicErrorHandling.runGuarded(body, [onError]) — the lifelong, outermost runZonedGuarded wrap around runApp. onError defaults to recordZoneError, so the common case is DreamicErrorHandling.runGuarded(() => runApp(...)) (ERH-001 / BEH-1).
    • DreamicErrorHandling.recordZoneError(error, stackTrace) — the public record entry the zone's onError (and the web-JS handlers) route through; forwards into the private chokepoint.
    • DreamicErrorHandling.installEarlyWebErrorHandlers() — apply-once install of the Dart window 'error'/'unhandledrejection' listeners (no-op on VM/mobile via a conditional import). Call at boot-step-3 only under Path A′ (see Web capture below). Web errors are serialized to a typed WebJsError (message verbatim for redaction; stack via StackTrace.fromString with a StackTrace.current fallback) and routed through the chokepoint; each handler suppresses the browser default via event.preventDefault().
    • recordCapturedError(error, stackTrace) (top-level in app/helpers/app_errorhandling_init.dart) — the public forwarder into the still-private _recordErrorSafe chokepoint.
  • CompositeErrorReporter(List<ErrorReporter> reporters) (lib/error_reporting/composite_error_reporter.dart) — a dreamic-public primitive that runs multiple backends concurrently (e.g. Sentry + Crashlytics). Fans every call (recordError, recordFlutterError, addBreadcrumb, setUser, clearUser) to each child in its own try/catch (one child's failure never blocks the others — BEH-2, BEH-11). initialize() uses Future.wait(..., eagerError: false) over Future.sync(() => child.initialize()) so a child that throws synchronously is caught too; a failed child is logged (console only) and skipped on every subsequent call (ERH-023, ERH-043). The flags (reporterRequiresFirebase / customReporterManagesErrorHandlers) still live on ErrorReportingConfig; the consumer passes the OR'd values across the children it composes (ERH-007).
  • AppConfigBase.breadcrumbLevel — a new LogLevel getter mirroring logLevel (dart-define breadcrumbLevel → Remote Config breadcrumbLevel → default info), evaluated at breadcrumb-emit time and independent of the console logLevel. The valid domain is restricted to {debug, info, warn, error} — validated inline in the getter (an out-of-domain value, incl. debugVerbose, falls back to info with a one-time console warning), NOT via configBounds (ERH-008, ERH-019, ERH-027). breadcrumbLevelDefault setter added. Lowering it to debug via Remote Config promotes logd to breadcrumbs with no redeploy (BEH-3, BEH-4). Deferred deployment step (ERH-044): seed the breadcrumbLevel RC key (default info, domain {debug,info,warn,error}) per environment to exercise BEH-4; until seeded the getter falls back to info (no key ⇒ default, not an error).
  • Logger.breadcrumb() / logBreadcrumb() gained an optional LogLevel level = LogLevel.info param (ERH-031). Existing call sites are unchanged (non-breaking). A breadcrumb below breadcrumbLevel is dropped at emit time; gating uses LogLevel only — never a backend level type (the LogLevelSentryLevel map is Sentry-side — ERH-026).

Behavior changes (internal, no API change for existing callers)

  • Central fail-closed redaction in Logger (redactErrorForReporting + breadcrumb redaction). The message and every value in a breadcrumb's data map are scrubbed (v1 patterns: oobCode, Bearer/token, email-in-URL) before forwarding. On any redaction failure it fails closed — the field is replaced with a placeholder derived from the error's runtimeType (e.g. [redaction-error: NotAllowedError]), never toString() (which could embed a secret), with a console-only diagnostic; nothing unredacted is ever forwarded (ERH-005, ERH-017, ERH-047 / BEH-8). Redaction now also runs on the loge()_crashReport forward path, so a direct loge(), the early-error buffer flush, and deferred bootstrap diagnostics all get the same scrubbing as the chokepoint (the original object is returned untouched in the common no-secret case, so the loge() regression path is unchanged).
  • Error chokepoint (_recordErrorSafe, reached via the public recordZoneError / recordCapturedError): a module-level re-entrancy guard (_reportingInFlight — an error raised while reporting is console-only, never re-entered), a bounded (error, stackTrace)-identity dedup set (capacity 20, FIFO) checked on the original identity before redaction (so a fail-closed placeholder can never collapse two distinct errors — ERH-003, ERH-028 / BEH-9), and a pre-attach buffering fallback so zone/web-JS/isolate errors fired before attach are retained (ERH-011 / BEH-12).
  • Pre-attach early-breadcrumb buffer (_earlyBreadcrumbBuffer, ~50 cap, drop-oldest) parallel to the early-error buffer. Logger.breadcrumb() buffers into it (after gate + redaction) when no reporter is attached; on attach appInitErrorHandling() flushes breadcrumbs first, then errors so each early error carries its preceding breadcrumb context, and flushed breadcrumbs are not re-redacted (ERH-022, ERH-032 / BEH-12).
  • Isolate listener re-routed (mobile) — the existing Isolate.current.addErrorListener now calls the chokepoint (_recordErrorSafe) instead of the reporter directly, so isolate errors get the same re-entrancy guard, dedup, and redaction (ERH-021 / BEH-1, BEH-9, BEH-11). Registered at attach, apply-once; it does not buffer pre-attach isolate errors (those are covered by the early FlutterError.onError / PlatformDispatcher.onError handlers — ERH-029).
  • Wakelock is mobile-only. WakelockPlus.enable() in app_configs_init.dart is now guarded with if (!kIsWeb) (hard, non-configurable, no web re-enable path) — web never requests wake lock, so the production web NotAllowedError ("Document is hidden") is gone. The .catchError is retained for mobile defensiveness (BEH-6).

Web capture mechanism — Path A′ (the recorded decision)

Excluding the Sentry JS SDK's web globalHandlersIntegration via the public options API is infeasible (ERH-042, supersedes ERH-002). The web capture mechanism is therefore a build-time consumer commitment, gated by a single consumer constant kWebDartCapture that gates both the boot-step-3 installEarlyWebErrorHandlers() call and the reporter's SentryFlutter.init web config (so they can never disagree). The error-reporting-hardening spike passed ⇒ Path A′ (Dart owns web capture):

  • On web, set options.autoInitializeNativeSdk = false and assign an explicit options.transport = HttpTransport(options, RateLimiter(options)). The flag alone silently drops all web events (it forces a now-inert JS-bound transport) — the explicit transport is mandatory. The Dart web-JS handler then becomes the sole web surface, so web errors gain Dart context
    • breadcrumbs + redaction + dedup + exactly-once (BEH-1/5/8/9 on web).
  • Path-A′ cost: HttpTransport and RateLimiter are not exported by package:sentry (only the abstract Transport is). Import them via package:sentry/src/transport/http_transport.dart + .../rate_limiter.dart with // ignore: implementation_imports, depend_on_referenced_packages (stable for the pinned sentry_flutter 9.22.0 — re-verify on any bump), OR wrap a small custom public Transport.
  • This config lives in the consumer's SentryFlutter.init and the canonical Sentry example — not in dreamic-public (which has no Sentry dependency). The web-JS handler module is built in dreamic regardless; under Path C you would omit the web block + the early-install call and keep the JS SDK as the web surface (BEH-5/8/9 re-scoped to mobile/best-effort-web).

Canonical examples + retired templates (ERH-024)

  • lib/error_reporting/error_reporter_example.dart is now the single source of truth: COMMENTED Sentry, Crashlytics, and Sentry+Crashlytics CompositeErrorReporter examples carrying the full hardening — maxBreadcrumbs = 250 on all platforms (Part 3.3 / BEH-10), the beforeBreadcrumb/beforeSend redaction safety net, the Sentry-side LogLevelSentryLevel map, the breadcrumb ingress contract, the runGuarded boot wiring, and the Path-A′ web-capture config. Backend imports are kept commented so dreamic-public stays dependency-free.
  • The earlier private Sentry and Crashlytics reporter templates are retired (replaced by a one-line pointer to the canonical examples above).

Migration

  • Wrap runApp in the guarded zone. In main(), after installEarlyErrorHandlers() → (Path A′ only) DreamicErrorHandling.installEarlyWebErrorHandlers()configureErrorReporting(...), change runApp(...) to DreamicErrorHandling.runGuarded(() => runApp(...)).
  • Set maxBreadcrumbs = 250 on all platforms in your SentryFlutter.init (remove any web-only / lower cap). Measure against the ~1 MB per-event limit at breadcrumbLevel = debug; lower (e.g. 150) if a verbose event approaches it.
  • Route all breadcrumbs through Logger.breadcrumb() / logBreadcrumb() — remove any direct Sentry.addBreadcrumb() calls (the ingress/redaction contract). Pass level: to set a breadcrumb's level (default info).
  • Add the redaction safety net (beforeBreadcrumb / beforeSend) and the Sentry-side LogLevelSentryLevel map to your Sentry reporter (see the canonical example).
  • Wire the web-capture path (Path A′): on web set autoInitializeNativeSdk = false + explicit options.transport = HttpTransport(options, RateLimiter(options)), and call DreamicErrorHandling.installEarlyWebErrorHandlers() in main() at boot-step-3 — both gated by your single kWebDartCapture constant.
  • Multi-backend (optional): wrap reporters in CompositeErrorReporter([...]) and pass the OR'd requiresFirebase / managesOwnErrorHandlers flags in the ErrorReportingConfig.
  • Wakelock: no action — dreamic now guards it !kIsWeb for you. Drop any app-side web-only wakelock hotfix.
  • breadcrumbLevel Remote Config (deferred): seed the key per environment (default info, domain {debug,info,warn,error}) to exercise BEH-4; until then it falls back to info.

0.10.0 #

Backend-agnostic error reporting (breaking) + Firebase App Check + startup resilience #

Three themes:

  1. Breaking — error reporting is now fully backend-agnostic. dreamic drops the firebase_crashlytics dependency and ships no built-in reporter; you register one ErrorReporter and dreamic routes every signal to it. Apps that relied on the built-in Crashlytics default must wire a reporter (drop-in template provided).
  2. Firebase App Check becomes a first-class, opt-in capability — attestation with no app-specific activation code.
  3. Startup resilience & diagnosability — the 0.9 splash-first startup is hardened against transient cold-start failures and a true hang is made diagnosable. Two default behaviors change here (auto-retry once; bounded Firebase init).

⚠️ Breaking: error reporting is backend-agnostic

  • firebase_crashlytics removed from dreamic's dependencies. dreamic no longer references any crash SDK, so it is no longer forced onto every consumer.
  • dreamic ships no concrete reporter and routes every signal — uncaught Flutter / platform / isolate errors, loge(), the early-error buffer, bootstrap diagnostics, and breadcrumbs — to the one ErrorReporter you register via configureErrorReporting(...). (Previously it dual-routed to Crashlytics and a custom reporter; now it routes to a single sink.)
  • ErrorReporter — breadcrumbs and user-context folded in. recordError is now the only required member; initialize, recordFlutterError (defaults to forwarding to recordError), addBreadcrumb, setUser, and clearUser all have default bodies. Use extends ErrorReporter so you override only what you support — implements ErrorReporter now requires all six. The separate ErrorReporterUserContext and ErrorReporterBreadcrumbs interfaces are removed (folded into ErrorReporter).
  • ErrorReportingConfig changes. Removed useFirebaseCrashlytics and the ErrorReportingConfig.firebaseOnly / .both constructors. Added reporterRequiresFirebase (bool, default false) — set true for a reporter that needs Firebase initialized first (e.g. Crashlytics) so dreamic attaches it after Firebase init; customOnly(...) gains a matching requiresFirebase parameter. A null customReporter (the default) means no reporting (console only).

Migration:

  • Crashlytics users: add firebase_crashlytics to your pubspec and implement a small ErrorReporter backed by FirebaseCrashlytics (override recordErrorrecordError, recordFlutterError, addBreadcrumblog, setUsersetUserIdentifier), then register it with requiresFirebase: true: configureErrorReporting(ErrorReportingConfig.customOnly(reporter: MyCrashlyticsReporter(), requiresFirebase: true, enableOnWeb: false)).
  • Sentry / custom reporters: change implements ErrorReporterextends ErrorReporter; turn any ErrorReporterUserContext setUser/clearUser and breadcrumb methods into @overrides of the now-built-in members and drop the implements ErrorReporterUserContext / ErrorReporterBreadcrumbs clauses.
  • No reporting: omit configureErrorReporting(...) (or pass a null reporter) — dreamic logs to the console only.

New: Firebase App Check (opt-in, off by default):

  • dreamic now owns App Check activation as a first-class bootstrap capability (like Remote Config). Opt in by passing an AppCheckConfig to dreamicBootstrap(appCheck: …); null (the default) skips App Check entirely, so existing apps are unaffected and need no changes. No app-specific activation code is required.
  • AppCheckConfig selects the right provider per platform (debug providers vs. AndroidPlayIntegrityProvider / AppleAppAttestWithDeviceCheckFallbackProvider / reCAPTCHA) with a keyless-web guard (an empty site key falls back to WebDebugProvider rather than throwing "Missing required parameters: sitekey" on a keyless release web build). Fields: webRecaptchaSiteKey, webRecaptchaEnterprise (default true → reCAPTCHA Enterprise, else v3), webProviderOverride / androidProviderOverride / appleProviderOverride (advanced — used verbatim), tokenAutoRefreshEnabled (default true), and activationTimeout (default 8s).
  • First-class, environment-aware config (no per-call wiring). The environment-varying inputs now resolve from AppConfigBase (dart-define → programmatic default — NOT Remote Config, since App Check activates before RC), so the typical call is appCheck: const AppCheckConfig():
    • APP_CHECK_DEBUGAppConfigBase.appCheckUseDebugProviders decides debug-vs-real attestation independently of kDebugMode — so a debug build can attest for real against staging/prod (e.g. flutter run --dart-define=ENVIRONMENT_TYPE=staging --dart-define=APP_CHECK_DEBUG=false). Default when unset: real everywhere except the emulator (emulator environment / doUseBackendEmulator), because the emulator bypasses App Check so real attestation there is pointless.
    • APP_CHECK_RECAPTCHA_SITE_KEYAppConfigBase.appCheckRecaptchaSiteKey is used when AppCheckConfig.webRecaptchaSiteKey is empty (reCAPTCHA site keys are public, so a dart-define / per-flavor value is not a secret).
    • APP_CHECK_ENABLEDAppConfigBase.appCheckEnabled (default true) is a per-build kill switch: disable activation for a single build without removing the config from main(). (App Check is still entirely off when no config is passed.) The *Override fields remain the verbatim escape hatch. (App Check enforcement is a server/console-side setting per project; activating on the client only attests — it does not enforce.)
  • Activation is bounded + never fatal. App Check is consumed lazily (the token is fetched on the first attested backend call; enforcement is handled by Firebase, not the client), so hard-blocking boot on activation would brick the app on a transient reCAPTCHA/network hiccup for zero security gain. A timeout or failure is reported (via the deferred-diagnostic path below, so a post-Firebase reporter such as Crashlytics sees it too) and boot continues; App Check then attests lazily. Activated early (right after afterFirebaseInit, before remote config / services) so it is in place before any attested call.

New API (optional, non-breaking):

  • DreamicAppInitHost.autoRetryCount (int, default 1) and .autoRetryDelay (Duration, default 800ms) — on a bootstrap failure the host silently re-runs the bootstrap, keeping the same splash up (no error-screen flash), up to autoRetryCount times before falling back to the manual errorBuilder. A transient cold-start failure (a contended IndexedDB lock on web, a flaky first network call) self-heals the same way a manual Retry tends to. The budget is per host lifetime and is not replenished by a manual retry. Behavior change: with the default 1, a deterministic failure surfaces the error screen one bootstrap cycle later than before. Set autoRetryCount: 0 to restore the previous show-error-on-first-failure behavior.
  • dreamicBootstrap(firebaseInitTimeout: …) (Duration?, default 30s) and dreamicBootstrap(firebaseRecoverIfRegisteredAfter: …) (Duration?, default 4s) — a two-tier bound on Firebase init alone, threaded into appInitFirebase as settleTimeout + recoverIfRegisteredAfter. Targets a confirmed returning-device iOS WebKit cold-start hang: Firebase.initializeApp() registers the app, then firebase_auth_web's onWaitInitState permanently blocks on the persisted-session IndexedDB read of firebaseLocalStorageDb (whose callback never fires on iOS WebKit), so initializeApp hangs forever. Tier 1 (grace, 4s): if init hasn't settled but the app IS registered, recover immediately via Firebase.app() — the same short-circuit a manual retry takes — catching the permanent hang fast. Tier 2 (settle, 30s): if the app is NOT yet registered at the grace (a slow-but- healthy SDK load), keep waiting the remainder rather than false-tripping a slow cold start into a retry; on the bound, recover if finally registered, else throw a diagnosable TimeoutException into the host's auto-retry / error path. A registered-app recovery is reported (not swallowed). 30s never legitimately trips. Pass firebaseInitTimeout: null to disable (the grace is then ignored).
  • appInitFirebase(options, {Duration? settleTimeout, Duration? recoverIfRegisteredAfter}) — the underlying two-tier bound. Both null (the defaults for direct callers) preserve the prior unbounded await, so direct callers are unaffected.
  • dreamicBootstrap(attachErrorReportingFirst: …) (bool?, default null) — controls whether the error reporter attaches before Firebase init (catching a step-1 / afterFirebaseInit failure, including the outer hang-timeout firing during them) or after it. The default null derives the right choice from the configured error-reporting config: attach early iff there is a reporter that does not require Firebase (reporterRequiresFirebase: false, e.g. Sentry). So a self-contained (Sentry-style) consumer gets maximal startup error coverage for free, while a Firebase-dependent reporter (e.g. Crashlytics with requiresFirebase: true) keeps the post-Firebase attach. Pass an explicit true/false to override the derivation. For the derivation to see your config, call configureErrorReporting(...) before dreamicBootstrap (the canonical pre-runApp main() line).
  • runBoundedNonCritical(work, {required timeout, required label}) — a public helper that runs non-critical post-init hook work with a hard timeout, swallowing a throw OR a timeout (reported via the deferred-diagnostic path, tagged with label) so it can never abort or stall the bootstrap. Use it for fire-and-settle setup (an analytics warm-up, a timezone service, …) inside a hook; the in-flight Future is not cancelled (Dart cannot) and completes in the background. (App Check no longer needs this — it is now first-class above.)
  • DreamicAppInitGate.onInitError (void Function(Object)?) — a notification fired once when the init Future errors, after the error is loge'd and before the gate transitions to its error branch. It does not suppress errorWidget; it lets a parent re-mount the gate before the error branch paints. This is how DreamicAppInitHost drives its auto-retry.
  • Breadcrumbs: logBreadcrumb(message, {category, data}) (and Logger.breadcrumb) records a trail event to the registered reporter for context leading up to a later error (e.g. the Firebase-init sequence before a startup-hang report). It routes to ErrorReporter.addBreadcrumb — a reporter that doesn't override it (or no reporter at all) is a silent no-op. dreamic emits bootstrap breadcrumbs internally.

Diagnosability:

  • The outer bootstrapTimeout now throws a TimeoutException that names the step in flight (dreamicBootstrap hung for 45s during step "…") instead of a bare "Future not completed" — so a hang (which has no stack trace of its own) is diagnosable. With the early reporter attach, even a hang during Firebase init reaches the backend.
  • Deferred bootstrap diagnostics close the "silent post-Firebase recovery" gap. A diagnostic that fires before the reporter attaches — the appInitFirebase registered-app recovery (Firebase step) and an App Check activation failure (right after) — is now buffered (bounded, drop-oldest) and flushed to the reporter the instant appInitErrorHandling attaches; later ones report immediately. Previously these reached only a reporter attached before Firebase; a reporter attached after Firebase (e.g. Crashlytics) now sees them too.

Dependencies:

  • Removed firebase_crashlytics (breaking — see above). dreamic no longer depends on any crash SDK.
  • Added firebase_app_check: ^0.4.5 under dependencies: — powers the new first-class App Check capability; inert unless you pass an AppCheckConfig to dreamicBootstrap.

0.9.2 #

  • Fixed: error handlers no longer blind debug tooling. Every FlutterError.onError handler installed by appInitErrorHandling / installEarlyErrorHandlers / _setupMinimalErrorHandlers now forwards to FlutterError.presentError in debug builds. Previously these branches (notably the emulator / no-reporting branch used during normal local dev) replaced onError with a handler that only called loge(), which suppressed the framework's structured error event. As a result DevTools / the IDE runtime-error inspector saw nothing, and the console lost the standard error block including the "relevant error-causing widget" attribution. The change is debug-only (kDebugMode), so release consoles and crash reporters are unaffected.

0.9.1 #

  • Fixed: the example model (enum_example.dart) now acquires Firestore via the canonical AppConfigBase.firestore getter instead of FirebaseFirestore.instance, so apps configured to use a non-default Firestore database are respected. The field is now late so it resolves on first use (after Firebase initialization).
  • Added: a guard test ensuring lib/ acquires Firestore via AppConfigBase.firestore (no direct FirebaseFirestore.instance outside app_config_base.dart).

0.9.0 #

Splash-first startup gate + bootstrap pipeline (breaking) #

Inverts cold-start so a branded splash paints on the first frame and the init chain runs behind it, eliminating the multi-second white launch screen. Adds a router-agnostic app-init gate, a retry-host, a branded splash widget with a seamless native→Flutter handoff, a bootstrap pipeline with two-phase error handling, and consolidates version gating onto the shell. This is a breaking 0.x minor — consumers opt in by bumping to ^0.9.0.

Raised SDK baseline (breaking): the minimum is now Flutter 3.44.0+ / Dart 3.12.0+ (previously Flutter 3.41.0 / Dart 3.11.0). This was required by the new flutter_native_splash 2.4.8 runtime dependency, which needs meta ^1.18.0 — a constraint Flutter 3.41.x's flutter_test (pinned meta 1.17.0) cannot satisfy. Apps depending on dreamic must now be on Flutter 3.44.0+ / Dart 3.12.0+.

  • Minimum environment raised to sdk: ^3.12.0 and flutter: ">=3.44.0".

New public API:

  • DreamicAppInitHost — the canonical runApp() argument. Shows a splash while the bootstrap Future runs, mounts child (your *App.router) on success, and shows error UI with a retry on failure. Owns the retry state (a fresh Key + Future per generation; initFutureFactory is called once per generation, never on a plain rebuild). errorBuilder is optional — omitted, the gate shows a built-in default error widget (ErrorWidget in debug, SizedBox.shrink() in release).
  • DreamicAppInitGate — the single-shot gate primitive (relocated from router_arc, router-agnostic). Holds the splash until max(initFuture, minimumSplashDuration) on success, but shows the error widget immediately on failure/timeout (the min-hold gates only success→child). On init error it loge()s + reports before showing the error widget (a handled .then(onError:) async error otherwise bypasses the global handlers). Wraps its splash/error branches in a default Directionality so they render above your *App.router with no inherited localizations.
  • DreamicSplash — a plain, branded splash widget that owns the native→Flutter handoff. Removes the native launch screen once its logo is decoded (no background-only flash), bounded by a safety timeout, routed through a single at-most-once guard across its three trigger paths (decode listener, timeout, dispose). const DreamicSplash() works with zero config when both it and your flutter_native_splash: codegen point at assets/splash_logo.png. Supports a custom child, an animated-logo Widget, and an optional removeNativeSplashWhen readiness hook.
  • dreamicBootstrap() — runs the init chain (Firebase → error backend attach + buffer flush → remote config → app configs → emulator → DreamicServices.initializeappInitAppCubit) behind the splash, with four ordered hook seams (afterFirebaseInit, registerBeforeServices, registerAfterServices, captureEntryIntents). Composes an outer hang-timeout (bootstrapTimeout, default 45s, nullable to disable). Every dreamic-core step is idempotent so a gate retry recovers. appInitAppCubit now runs inside dreamicBootstrap() — you no longer call it yourself.
  • installEarlyErrorHandlers() — synchronous pre-runApp early error handlers that buffer errors (bounded, drop-oldest, ~50 cap) before Crashlytics attaches; appInitErrorHandling() flushes the buffer to the reporter once it attaches.

Removed (breaking):

  • appRunIfValidVersion (the runApp-wrapper) and OutdatedApp / OutdatedAppPage (the startup-gate page) are deleted, along with their barrel exports. A too-old app is now blocked by the shell's existing AppStatus.updateRequiredAppUpdateDialog path (reading the same minimumAppVersionRequired* Remote Config keys), which now also fires at cold start. appIsVersionValid is retained (it backs AppVersionUpdateService).

Contract changes:

  • Sticky updateRequired. The version-update emission now reaches a live listener at cold start (the AppCubit version-update subscriber is attached before AppVersionUpdateService().initialize()), and AppStatus.updateRequired is now sticky: no startup-status downgrade (_finalizeAppStartup's normal, both networkError emits, the error emits) overwrites it — so a too-old app stays blocked even offline. A mid-session Remote Config change that lowers the minimum lifts the block in-session (a VersionUpdateType.none event transitions updateRequired → normal). Without this, deleting appRunIfValidVersion would have left too-old users unblocked at launch.
  • DreamicServices.initialize re-entrancy. On a failure it now disposes and unregisters its just-constructed/early-registered services before rethrowing, and early-returns the cached result when already initialized — so a gate retry recovers cleanly without accumulating duplicate authStateChanges/FCM/lifecycle subscriptions and resolves the retry's live (not disposed) instances. NotificationService.initialize() recreates its _badgeCountController if a prior dispose() closed it (so badgeCountStream is live post-recovery), and DeviceServiceImpl gains a public dispose() teardown.
  • addOnAuthenticatedCallback replay-on-register. A callback added after the initial auth event has been delivered while a user is authenticated is now invoked once (via scheduleMicrotask) with the current uid — so an auth-lifecycle callback registered behind the gate (e.g. in registerAfterServices) still fires for an already-signed-in user on warm start. onAuthenticated-only (logout is not replayed).

Dependencies:

  • flutter_native_splash: ^2.4.8 added under dependencies: (runtime preserve/remove API used by DreamicSplash). Apps that run the native splash codegen also add it as a direct dev_dependency.

Consumer migration (one-liner): replace appRunIfValidVersion(() => const MyApp()); with runApp(DreamicAppInitHost(initFutureFactory: () => dreamicBootstrap(...), splash: const DreamicSplash(), child: const MyApp())); and wire AppVersionUpdateService on your shell for the too-old block. See dreamic-initial-setup.md Section 6 for the full canonical main.dart.

0.8.0 #

Dependency upgrades and raised SDK baseline #

Brings the full dependency set up to current releases. One breaking change for consumers: the minimum SDK baseline has been raised — apps depending on dreamic must now be on Flutter 3.41.0+ / Dart 3.11.0+ (previously Flutter 3.38.1 / Dart 3.10). This was required to adopt open_file 4.0.0, which sets those minimums.

Breaking:

  • Minimum environment raised to sdk: ^3.11.0 and flutter: ">=3.41.0".

Major upgrades:

  • flutter_local_notifications 21 → 22 — adds web platform support (dormant until wired up; all calls remain kIsWeb-guarded). No public API changes.
  • internet_connection_checker_plus 2 → 3 — now a pure-Dart package; the bundled connectivity_plus listener was removed. AppCubit now passes Connectivity().onConnectivityChanged as the triggerStream so connectivity changes are detected instantly (preserving the v2 behavior) rather than only on the periodic poll.
  • open_file 3 → 4 — Android drops the REQUEST_INSTALL_PACKAGES permission check; the OpenFile.open API is unchanged.

Firebase suite: firebase_core 4.10.0, firebase_auth 6.5.2, firebase_storage 13.4.2, cloud_firestore 6.5.0, cloud_functions 6.3.2, firebase_messaging 16.3.0, firebase_remote_config 6.5.2, firebase_crashlytics 5.2.3. Note: firebase_core 4.10.0 bumps the native Firebase SDKs (iOS 12.14.0 / Android 34.14.0) — run pod install --repo-update on iOS when adopting.

Minor / patch: adaptive_dialog 2.8.0, app_badge_plus 1.3.1, bloc 9.2.1, device_info_plus 13.1.0, flutter_timezone 5.1.0, json_annotation 4.12.0, json_serializable 6.14.0, package_info_plus 10.1.0, permission_handler 12.0.3, wakelock_plus 1.6.1, build_runner (dev) 2.15.0.

0.7.5 #

New Feature: Responsive utilities (device-class detection + value-by-breakpoint) #

Adds a small, self-contained responsive toolkit — a zero third-party-dependency replacement for the thin slice of responsive_framework the app actually used (device-class detection and "pick a value by breakpoint"). It is optional and non-breaking: existing apps are unaffected and need no changes.

The API exposes two reactivity models, kept distinct by name. Class-based reads resolve the ResponsiveScope's whole-window device class and rebuild only when the class flips (segment-only). Viewport-width helpers read the nearest MediaQuery width at the call site and rebuild per-pixel; the Viewport qualifier marks "tracks the local width here, not the device class."

New API (optional, non-breaking):

  • DeviceSize (enum { mobile, tablet, desktop }) — the current viewport-width / layout class. Orthogonal to DevicePlatform (the OS) and the internal device form factor.
  • Breakpoints — globally configurable device-class thresholds (Breakpoints.tablet / Breakpoints.desktop, defaults 600 / 1024 logical px). Configure once at startup before runApp; set tablet < desktop.
  • ResponsiveScope (Widget) — wrap the app once (top-level above MaterialApp is supported) to publish the whole-window device class to the subtree.
  • ResponsiveContext (extension on BuildContext):
    • deviceSize / isMobile / isTablet / isDesktop — the current layout width class (not the OS platform).
    • responsive<T>({required T mobile, T? tablet, T? desktop}) — pick a value by device class with fallback chaining toward the smaller class.
    • responsiveByViewportWidth<T>({required Map<double, T> breakpoints, required T fallback}) — pick a value by call-site viewport width (highest matching threshold wins).
    • clampByViewportWidth({required double minValue, required double maxValue, double minW = 360, double maxW = 1200}) — CSS clamp()-equivalent linear interpolation across a width range, clamped at both ends.

0.7.4 #

New Feature: Configurable Firestore database id (Enterprise-edition named databases) #

Adds support for pointing the app at a named Firestore database instead of the conventional (default) one. This is required for projects whose Firestore is an Enterprise-edition database, since Enterprise databases cannot use the (default) id and the SDK must target the named database explicitly.

The default is unchanged (null(default) database), so existing apps are unaffected and need no changes.

New API (optional, non-breaking):

  • AppConfigBase.backendFirestoreDatabaseId (String? getter) — the database id the app connects to. Resolves from the BACKEND_FIRESTORE_DATABASE_ID dart-define when provided, otherwise from backendFirestoreDatabaseIdDefault, otherwise null (the (default) database). Resolved once and cached.
  • AppConfigBase.backendFirestoreDatabaseIdDefault (String? setter) — programmatic default, set from your app's config before any Firestore use. Overridden by the BACKEND_FIRESTORE_DATABASE_ID dart-define when that is set.
  • AppConfigBase.firestore (FirebaseFirestore getter) — the app-wide Firestore instance, targeting backendFirestoreDatabaseId (or (default) when null). Throws StateError if accessed before appInitFirebase() initializes Firebase.

Recommended usage:

Use AppConfigBase.firestore everywhere instead of FirebaseFirestore.instance so projects on a named Enterprise database resolve to the correct database. Configure for a named database via either:

// Build flag
// flutter run --dart-define=BACKEND_FIRESTORE_DATABASE_ID=default

// Or programmatically, before any Firestore use
AppConfigBase.backendFirestoreDatabaseIdDefault = 'default';

Internal:

  • The Firebase emulator is now wired onto AppConfigBase.firestore (the same instance the app uses everywhere) instead of a freshly-created (default) instance, so a named database id resolves consistently for both emulator and live data access.

0.7.3 #

Hardening: Remote Config defaults can no longer crash app startup #

AppConfigBase.defaultRemoteConfig previously held two null entries (notificationGoToSettingsMaxAskCount, notificationValuePropReminderMaxAskCount). On the live Firebase Remote Config path, FirebaseRemoteConfig.setDefaults rejects any non-bool/num/String value, so a null default threw an ArgumentError that propagated out of appInitRemoteConfig and prevented the app from reaching runApp (white screen) — and only on a live deploy, since the mock/emulator path never calls setDefaults.

Three complementary guards now prevent this:

  • Compile guard: defaultRemoteConfig is now Map<String, Object>, so a null (or other nullable) literal value fails to compile.
  • Sentinel for "unlimited": the two notification max-ask-count defaults are now stored as the -1 sentinel (AppConfigBase.notificationMaxAskCountUnlimited) instead of null, mapped back to null at the getter boundary. The public int? getter contract is unchanged (null = unlimited); every other stored value, including 0, passes through unchanged.
  • Loud runtime guard: appInitRemoteConfig now validates the merged defaults (DreamIC's plus additionalDefaultConfigs) on every init path (live, mock, emulator) before any GetIt registration, throwing an ArgumentError that names each offending 'key' = RuntimeType if a value is not bool/num/String. A bad consumer default now surfaces in local development instead of first crashing a live deploy.

Breaking-ish notes:

  • AppConfigBase.defaultRemoteConfig is now Map<String, Object> (was Map<String, dynamic>). Consumers assigning the field a Map<String, dynamic> or mutating it with a nullable value will get a type error.
  • appInitRemoteConfig now throws an ArgumentError on unsupported additionalDefaultConfigs values on all paths (live, mock, emulator), so a bad default surfaces in local development instead of first crashing a live deploy.

Consumers that previously worked around the crash by setting both notification max-ask-count values to a positive number (e.g. 10) before calling appInitRemoteConfig can remove that workaround after upgrading.

0.7.2 #

Bug fix: Auto-wire FCM token persistence in DreamicServices.initialize #

Auto-wire FCM token persistence to DeviceService when both services are enabled via DreamicServices.initialize.

Previously, DreamicServices.initialize(enableDeviceService: true, enableNotifications: true) left NotificationService._onTokenChanged unset, so the silent FCM-token capture path on login — taken when the user had already granted permission and AppConfigBase.fcmAutoInitialize is false — never persisted the token to the device document. Apps relying on the canonical delegate-to-DeviceService pattern got a silently-broken token flow unless they also called connectToAuthService separately.

DreamicServices.initialize now resolves an effective onTokenChanged when both services are enabled and the caller didn't supply one, defaulting to DeviceServiceInt.persistFcmToken. The legacy connectToAuthService path is unchanged.

New API (optional, non-breaking):

  • DreamicServices.initialize — new optional onTokenChanged parameter matching NotificationService.initialize's shape. Omit for the auto-wired default; pass to override or chain custom logic.
  • DreamicServices.defaultTokenChangedCallback(DeviceServiceInt) — public static helper that returns the delegate-to-persistFcmToken callback. Apps that want to wrap the default (e.g., add analytics) can compose with it directly.

Notification Permission: Value-Prop Decline Tracking #

Adds a third notification-permission lifecycle cohort — value-prop declines — alongside the existing system-denied and go-to-settings cohorts. runNotificationPermissionFlow() now auto-records when the user taps "Not Now" on the in-app value-proposition sheet (before the OS dialog is ever shown), and auto-clears the tracking when permission is later granted.

New API:

  • ValuePropDeclineInfo data class — lastDeclineTime, declineCount, with JSON round-trip and copyWith.
  • NotificationService.getValuePropDeclineInfo() / clearValuePropDeclineInfo() / recordValuePropDecline() — mirrored on the service for parity with the existing two cohorts (also available directly on NotificationPermissionHelper).
  • NotificationService.shouldShowValuePropReminder({Duration? cooldown, int? maxAskCount}) — predicate to decide whether to re-prompt a previously-declined user. Gates on permissionStatus == notDetermined; returns false for already-decided cohorts. See the doc-comment for full edge-value semantics (maxAskCount: 0 / negatives = never re-prompt; cooldown: Duration.zero = always passes timing gate; null for either = fall through to AppConfigBase value).

Auto-integration:

  • runNotificationPermissionFlow() records a value-prop decline iff it returns NotificationFlowResult.declinedValueProposition; never on any other result (including skippedAskAgain — that's the system-denied cohort declining the re-ask dialog).
  • autoClearIfGranted(), the OS settings deep-link handler, and the app-resume detection path all clear the value-prop tracking blob alongside the existing two when permission is granted.

NotificationSettingsDeepLinkInfo.permissionJustGranted semantic widened:

  • Was: true when stored denial tracking data existed before the deep link AND permission is now authorized.
  • Now: true when any stored tracking data (denial info, go-to-settings prompt info, or value-prop decline info) existed before the deep link AND permission is now authorized.
  • This retroactively fixes a pre-existing gap where a user who saw only the go-to-settings prompt (no system denial) would not have triggered the flag.

AppConfigBase retrofit — four new Remote-Config-tunable keys:

  • notificationGoToSettingsAskAgainDays (int, default 30, bounds 1–365) — formerly inline-only in NotificationFlowConfig.fromAppConfig().
  • notificationGoToSettingsMaxAskCount (int?, default null = unlimited, bounds 1–100 when set) — formerly inline-only.
  • notificationValuePropReminderCooldownDays (int, default 30, bounds 1–365) — new.
  • notificationValuePropReminderMaxAskCount (int?, default null = unlimited, bounds 1–100 when set) — new.

The two *MaxAskCount keys introduce a new int? getter pattern in AppConfigBase, with a layer-specific convention: RC value <= 0 (including the 0 Firebase RC returns for unset keys) means "unset, fall through to programmatic default." This is independent of the inline helper API's maxAskCount: 0/negative = never re-prompt contract — the two layers have distinct signatures (RC is non-nullable int; inline is int?) and both are documented.

Behavior change for non-default callers. Existing apps that neither pass inline overrides nor set the new *Default setters see identical behavior — the AppConfigBase defaults match the prior Dart defaults (30 days, null). Apps that previously passed null inline for NotificationFlowConfig.fromAppConfig().goToSettingsMaxAskCount AND set a non-null notificationGoToSettingsMaxAskCountDefault will now see the AppConfigBase value take effect (where previously inline null short-circuited to "unlimited"). If you relied on inline null meaning "force unlimited regardless of AppConfigBase," explicitly pass a large int instead.

0.7.1 #

Widened version constraints on wakelock_plus and package_info_plus so consuming apps can stay on the win32 ^5 line when they transitively depend on packages that haven't migrated yet (e.g. quill_native_bridge_windows, file_picker). The 1.5.x/9.x and 1.6.x/10.x branches share the same Dart APIs — the majors were cut solely to bump win32 from ^5 to ^6.

  • wakelock_plus: ^1.6.0">=1.5.2 <2.0.0"
  • package_info_plus: ^10.0.0">=9.0.0 <11.0.0"

0.7.0 #

⚠️ Breaking Changes: Minimum SDK Bumps #

Raised minimum toolchain versions to match upgraded dependencies:

  • Dart SDK: >=3.10.0 (was ^3.5.4)
  • Flutter SDK: >=3.38.1 (was >=3.22.0)

Consumer platform minimums (enforced by flutter_local_notifications 21.0.0):

  • Android: minSdk 24 (Android 7.0) — was 21
  • iOS: 13 — was 11
  • macOS: 10.15 — was 10.14

⚠️ Breaking Changes: flutter_local_notifications 21.0.0 #

flutter_local_notifications v20 converted several methods from positional to named parameters. Internal dreamic call sites have been migrated:

  • FlutterLocalNotificationsPlugin.initialize(settings, ...) now takes settings: as a named parameter
  • FlutterLocalNotificationsPlugin.show(id, title, body, details, ...) now takes all args as named
  • FlutterLocalNotificationsPlugin.cancel(id) now takes id: as a named parameter
  • AndroidFlutterLocalNotificationsPlugin.deleteNotificationChannel(channelId) now takes channelId: as a named parameter

Consumer apps calling these APIs directly must update their call sites. Apps using only dreamic's NotificationService / NotificationChannelManager require no code changes.

_createActionCodeSettings renamed _createActionCodeSettingsAsync).

Choose one of the two approaches in your app initialization:

Option 1 — Plain App Links / Universal Links (most common):

AppConfigBase.emailConfirmMobileOriginDefault = 'https://app.example.com';

The origin must match the App Links / Universal Links domain configured in AndroidManifest.xml intent-filters and iOS associated domains. The /emailconfirm path is appended automatically and must match the route handler in your consuming app.

Option 2 — Firebase Dynamic Links, linkDomain, Branch, AppsFlyer, etc.:

AppConfigBase.mobileEmailLinkActionCodeSettingsBuilder = () async {
  final pkg = await AppConfigBase.getPackageInfo();
  return ActionCodeSettings(
    url: 'https://your-underlying-url/emailconfirm',
    linkDomain: 'links.example.com', // or dynamicLinkDomain, or wrap in Branch URL
    androidPackageName: pkg.packageName,
    iOSBundleId: pkg.packageName,
    handleCodeInApp: true,
  );
};

Other Changes #

  • Web email-link continueUrl is now derived from Uri.base — no configuration required for web.
  • If neither mobile option above is set, signInWithEmail throws StateError at the first mobile email-link auth attempt with a message naming both options.
  • Added PhoneAuthError.smsCodeExpired and .sessionExpired for granular phone verification error handling.
  • Added AuthServiceSignInFailure.signInTimedOut — returned when post-registration sign-in retry loop exhausts all attempts.
  • Removed deprecated sharedPrefKeyTimezone constant and sign-out cleanup code (timezone tracking handled by DeviceService since v0.4.0).

Planned #

  • Begin extraction of auto_route singleton helpers to dreamroute_autoroutehelper.
  • Compatibility policy locked to temporary re-exports in dreamic with deprecation for one minor release cycle.
  • Planned minimum compatible pair: dreamic >=0.7.0 <0.8.0 with dreamroute_autoroutehelper >=0.1.0 <0.2.0.

0.5.0 #

Added #

  • DreamicServices.initialize for race-free, ordered service initialization (with prioritized auth/logout callbacks)
  • New Device Service foundation: stable device ID, device models (DeviceInfo, DevicePlatform), timezone/offset tracking with offline pending payload handling, and comprehensive tests

Changed #

  • Auth lifecycle callbacks now support prioritized execution via PrioritizedCallback
  • Improved DeviceService/NotificationService coordination to avoid auth/logout race conditions

0.4.0 #

⚠️ Breaking Changes: FCM Token Management #

Overview

FCM (Firebase Cloud Messaging) token management has been moved from AuthServiceImpl to NotificationService. This provides better separation of concerns and enables the deferred permission prompt feature.

Breaking Changes

  • REMOVED: useFirebaseFCM parameter from AuthServiceImpl constructor
  • MOVED: FCM token management (registration, refresh, cleanup) to NotificationService
  • CHANGED: AppConfigBase.useFCMWeb now defaults to false (web FCM is opt-in, requires VAPID setup)
  • CHANGED: AppConfigBase.fcmAutoInitialize now defaults to false (see below)

Migration Required

Old Pattern (REMOVED):

GetIt.I.registerSingleton<AuthServiceInt>(
  AuthServiceImpl(
    firebaseApp: fbApp,
    useFirebaseFCM: !kIsWeb,  // This parameter no longer exists
    onAuthenticated: (uid) async { ... },
    onLoggedOut: () async { ... },
  ),
);

New Pattern:

// 1. AuthServiceImpl - no FCM parameters
GetIt.I.registerSingleton<AuthServiceInt>(
  AuthServiceImpl(
    firebaseApp: fbApp,
    onAuthenticated: (uid) async { ... },
    onLoggedOut: () async { ... },
  ),
);

// 2. FCM is configured via AppConfigBase (optional)
AppConfigBase.useFCMDefault = true;      // Mobile: defaults true (false on iOS simulator)
AppConfigBase.useFCMWebDefault = true;   // Web: defaults false (requires VAPID setup)

// 3. NotificationService handles FCM tokens
await NotificationService().initialize(
  onNotificationTapped: (route, data) async { ... },
);
await NotificationService().connectToAuthService();  // Auto-syncs tokens on auth changes

New FCM Configuration Flags

  • AppConfigBase.useFCM - Master FCM toggle (auto-false on iOS simulator)
  • AppConfigBase.useFCMWeb - Web-specific FCM toggle (defaults false)
  • AppConfigBase.fcmAutoInitialize - Auto-request permission on login (now defaults false)
  • Can be set via code or build flags: --dart-define USE_FCM=true

Deferred Notification Permissions (New Default)

fcmAutoInitialize now defaults to false - consuming apps must explicitly request notification permissions at an appropriate time in their UX flow.

Old Behavior (auto-prompt on login):

// Permissions were automatically requested when user logged in
// No code needed - but poor UX (users hadn't seen value yet)

New Behavior (explicit permission request):

// After NotificationService().initialize(), call one of:

// Option 1: Full flow with value proposition dialog (recommended)
final result = await NotificationService().runNotificationPermissionFlow(context);

// Option 2: Direct permission request
final result = await NotificationService().initializeNotifications();

To restore old behavior:

// Before Firebase.initializeApp()
AppConfigBase.fcmAutoInitializeDefault = true;

// Or via build flag
// flutter run --dart-define FCM_AUTO_INITIALIZE=true

Benefits

  • Deferred Permissions: Request notification permission at optimal moments, not app launch
  • Better UX: Full control over permission prompts with customizable UI
  • Cleaner Auth: AuthServiceImpl focuses on authentication only
  • Automatic Cleanup: connectToAuthService() handles token sync on login/logout

See NOTIFICATION_GUIDE.md for complete setup and migration instructions.


⚠️ Breaking Changes: TappableAction Debounce/Throttle System #

Overview

Complete rewrite of the debounce/throttle system in TappableAction. The external tap_debouncer dependency has been removed and replaced with a comprehensive in-house implementation providing advanced timing control, concurrency modes, and observability.

Breaking Changes

  • REMOVED: tap_debouncer dependency from pubspec.yaml
  • DELETED: lib/app/helpers/debouncer_widget.dart (unused file)
  • REMOVED: DebouncerHandler class (replaced by AsyncExecutor)

New Core Primitives

All primitives support: enabled, resetOnError, debugMode, name, onMetrics, wrap(), cancel(), dispose().

Primitive Purpose
CallbackController Abstract base class for sync primitives
Throttler Execute immediately, block subsequent calls
Debouncer Wait for pause before executing (leading/trailing edge)
RateLimiter Token bucket algorithm with burst capacity
HighFrequencyThrottler Optimized for 16-32ms intervals (DateTime-based)
ThrottleDebouncer Combined leading + trailing execution
BatchThrottler<T> Aggregate multiple actions into batch
AsyncDebouncer Async debouncing with auto-cancellation
AsyncThrottler Process-based locking with timeout
AsyncExecutor Concurrency control (drop/replace/keepLatest/enqueue)

New Enums

enum TapExecutionMode {
  throttle,        // Execute immediately, block subsequent (default)
  debounce,        // Wait for pause before executing
  throttleDebounce,// Leading + trailing execution
  rateLimited,     // Token bucket rate limiting
  highFrequency,   // Optimized for 16-32ms intervals
}

enum TapConcurrencyMode {
  drop,       // Ignore new while processing (default)
  replace,    // Cancel current, start new
  keepLatest, // Keep current + queue latest only
  enqueue,    // Queue all, execute sequentially (FIFO)
}

Extended TappableActionConfig

New fields added (all with defaults for backward compatibility):

  • executionMode - Timing mode (defaults to throttle)
  • executeOnLeadingEdge - Execute on first call (defaults to true)
  • executeOnTrailingEdge - Execute after pause (defaults to false)
  • concurrencyMode - Async handler control (defaults to drop)
  • rateLimitMaxTokens, rateLimitRefillInterval, rateLimitTokensPerRefill - Rate limiter config
  • onMetrics - Callback for tap analytics
  • enabled - Enable/disable toggle
  • maxDuration - Timeout for async operations
  • debugName - Debug logging identifier

New Config Presets

// Search inputs - debounce with trailing edge
TappableActionConfig.search()

// Toggle buttons - immediate feedback, replace on rapid tap
TappableActionConfig.toggle()

// Sliders - rate limited with burst capacity
TappableActionConfig.slider()

Migration Guide

Existing code requires NO changes - all existing APIs are preserved:

// This still works exactly as before
TappableAction(
  config: const TappableActionConfig(
    requireNetwork: true,
    debounceTaps: true,
    coolDownDuration: Duration(milliseconds: 300),
    groupId: 'my-group',
  ),
  onTap: () => doSomething(),
  builder: (context, onTap) => ElevatedButton(
    onPressed: onTap,
    child: Text('Tap me'),
  ),
)

If using DebouncerHandler directly (rare), migrate to AsyncExecutor:

// Old (REMOVED)
final handler = DebouncerHandler();
await handler.execute(() async => doWork());

// New
final executor = AsyncExecutor(mode: ConcurrencyMode.drop);
await executor.execute(() async => doWork());
executor.dispose(); // Call in widget dispose

Benefits

  • -1 dependency: Removes tap_debouncer external package
  • Full control: In-house primitives with TimerFactory for testability
  • Rich features: 5 execution modes, 4 concurrency modes, metrics, debugging
  • Memory safe: Proper disposal, mounted checks, call ID tracking

0.3.5 #

New Features #

  • Added: Account linking support with linkEmailPassword() method in AuthServiceInt
    • Converts anonymous users to permanent email/password accounts while preserving UID
    • Returns typed AuthServiceLinkFailure enum for error handling
  • Added: Password update support with updatePassword() method in AuthServiceInt
    • Allows users to change their password (requires recent authentication)
  • Added: AuthServiceLinkFailure enum with comprehensive error cases:
    • userNotLoggedIn, emailAlreadyInUse, weakPassword, invalidEmail, invalidCredential, requiresRecentLogin, credentialAlreadyInUse, unexpected

0.3.4 #

Breaking Changes #

  • Renamed: App version methods in AppConfigBase for clarity:
    • getAppVersion()getPackageInfo()
    • getAppVersionString()getVersion()
    • getAppBuildNumber()getBuildNumber()
    • getAppRelease()getReleaseId()

New Features #

  • Added: getBuildInfo() method for detailed build info display (version+build with optional git/date info)
  • Added: getVersionForDisplay() method that returns simple version in production, full build info otherwise
  • Added: BUILD_DATE dart-define parameter for including build timestamps in release strings
  • Added: Notification state management to AppCubit with unread notification count and permission status in AppState
  • Improved: NotificationBadgeWidget now uses AppCubit state instead of polling, reducing complexity and improving reactivity

Enhancements #

  • Enhanced: AppRootWidget now uses Overlay for toasts, improving toast display reliability
  • Improved: ToastManager with null checks and better state management
  • Improved: Network checking now uses auth emulator port when running against Firebase emulator
  • Enhanced: Firebase initialization checks with better error handling and proper usage validation

Internal #

  • Reorganized: Files moved from app/helpers/ into appropriate domain folders
  • Updated: Error reporter example to use renamed version methods

0.3.3 #

Enhancements #

  • Added: Git build information support for error reporting with gitBranch, gitTag, and gitCommit properties in AppConfigBase
  • Added: getAppReleaseFullInfo() method for comprehensive release strings including git information (useful for Sentry)
  • Added: doDisableErrorReporting master kill switch for completely disabling error reporting
  • Added: doForceErrorReporting flag to enable error reporting even in emulator mode (for testing)
  • Improved: Remote Config listener recovery logic with transient error handling and prevention of overlapping recovery attempts
  • Improved: App version logging now includes full release information

Fixes #

  • Fixed: Error reporting configuration now properly respects all control flags in various development and testing scenarios

Documentation #

  • Updated: Error Reporting Guide with git build information setup and new control flags

0.3.2 #

Enhancements #

  • Added: Email verification support with isEmailVerified, sendEmailVerification(), and reloadUser() methods in AuthServiceInt
  • Added: Re-authentication support with reauthenticateWithPassword() for sensitive operations like changing email, password, or deleting account
  • Added: AuthServiceEmailVerificationFailure enum for handling email verification errors
  • Added: duplicateRecord case to RepositoryFailure enum

Fixes #

  • Fixed: Custom error reporters (Sentry, etc.) are now only initialized when shouldUseErrorReporting is true, preventing error capture when running in emulator mode
  • Fixed: Error reporting now respects DO_USE_BACKEND_EMULATOR environment variable by not initializing Sentry SDK in emulator mode

Documentation #

  • Updated: Error Reporting Guide with clarified conditional initialization flow
  • Cleaned: Removed legacy commented code from template snippets

0.3.1 #

Enhancements #

  • Added: Automated migration script migration_scripts/migrate_enum_converters.dart with dry-run support
  • Added: Comprehensive migration guide docs/ENUM_MIGRATION_GUIDE.md for AI-assisted migrations
  • Added: 27 integration tests in test/data/enum_serialization_test.dart proving unknown enum values don't crash
  • Improved: Documentation with complete examples for all three enum serialization strategies
  • Fixed: All example models in lib/data/models/enum_example.dart now use correct pattern

Migration Tools #

Run the automated migration script to convert old converter classes:

# Preview changes
dart run migration_scripts/migrate_enum_converters.dart --dry-run

# Apply migration
dart run migration_scripts/migrate_enum_converters.dart

Documentation #

  • docs/ENUM_MIGRATION_GUIDE.md - AI-readable migration guide for other projects
  • docs/ENUM_SOLUTION_ARCHITECTURE.md - Technical deep dive
  • docs/ENUM_QUICK_START.md - 5-minute quick start

0.3.0 #

⚠️ BREAKING CHANGE: Enum Serialization Rewrite #

Overview

Complete rewrite of enum serialization system. The previous converter class approach was fundamentally broken - json_serializable ignores @JsonConverter annotations on non-nullable enum fields. This caused crashes when servers sent unknown enum values.

Breaking Changes

  • REMOVED: RobustEnumConverter, NullableEnumConverter, DefaultEnumConverter, LoggingEnumConverter classes
  • ADDED: safeEnumFromJson<T>() and safeEnumToJson<T>() helper functions
  • CHANGED: Enum fields now require @JsonKey(fromJson:, toJson:) annotations instead of converter class annotations

Migration Required

Old Pattern (BROKEN):

enum Priority { low, medium, high }

class PriorityConverter extends DefaultEnumConverter<Priority> {
  const PriorityConverter();
  @override
  List<Priority> get enumValues => Priority.values;
  @override
  Priority get defaultValue => Priority.medium;
}

@JsonSerializable()
class TaskModel {
  @PriorityConverter()  // This was IGNORED by json_serializable!
  final Priority priority;
}

New Pattern (WORKS):

enum Priority { low, medium, high }

// Create helper functions
Priority _deserializePriority(String? value) {
  return safeEnumFromJson(
    value,
    Priority.values,
    defaultValue: Priority.medium,
  )!;  // Safe to use ! when defaultValue is provided
}

String? _serializePriority(Priority? value) {
  return safeEnumToJson(value);
}

@JsonSerializable()
class TaskModel {
  @JsonKey(fromJson: _deserializePriority, toJson: _serializePriority)
  final Priority priority;  // Now properly handled!
}

Three Strategies

1. Nullable (Unknown → null): For optional fields

UserRole? _deserializeUserRole(String? value) {
  return safeEnumFromJson(value, UserRole.values);
}

2. Default (Unknown → default value): For required fields

Priority _deserializePriority(String? value) {
  return safeEnumFromJson(
    value, 
    Priority.values, 
    defaultValue: Priority.medium,
  )!;
}

3. Logging (Unknown → log + default): For monitoring

Status _deserializeStatus(String? value) {
  return safeEnumFromJson(
    value,
    Status.values,
    defaultValue: Status.draft,
    onUnknownValue: (v) => logw('Unknown Status: $v'),
  )!;
}

Migration Steps

  1. Remove all enum converter classes from your models
  2. Create helper functions for each enum using the patterns above
  3. Update all enum fields to use @JsonKey(fromJson:, toJson:)
  4. Run code generation: dart run build_runner build --delete-conflicting-outputs
  5. Test with unknown enum values to verify graceful handling

See lib/data/models/enum_example.dart for complete working examples.

Why This Change

  • Previous approach did not work - crashes were inevitable with unknown enum values
  • New approach always works - @JsonKey is never ignored by json_serializable
  • Simpler for AI to implement - just helper functions, no class hierarchies
  • Full type safety maintained
  • Three clear strategies for different requirements

0.2.0 #

✨ New Feature: Comprehensive Notification Service #

Overview

Added a complete, production-ready notification system that abstracts away FCM complexity and eliminates ~300 lines of boilerplate code from consuming apps.

Added

  • NotificationService - Central service for managing all notification functionality

    • Lazy initialization (no side effects until explicitly initialized)
    • Automatic FCM message handling (foreground, background, terminated states)
    • Local notification display with platform-specific customization
    • Permission request management with iOS/Android/web support
    • Notification routing with deep link support
    • Badge count management across platforms
    • Permission state tracking and analytics
    • Periodic reminder system for denied permissions
  • Notification Models (lib/data/models/)

    • NotificationPayload - Complete notification data structure
    • NotificationAction - Action button definitions
    • NotificationPermissionStatus - Unified permission state enum
  • NotificationPermissionHelper - Permission management utilities

    • Permission status checking
    • Optimal timing suggestions for permission requests
    • Permission request history tracking
    • Platform-aware permission capabilities
  • Background Handler - Top-level FCM background message handler

    • dreamicNotificationBackgroundHandler - Ready-to-use background handler
    • Isolate-safe with automatic Firebase initialization
    • Extensible for custom background logic
  • UI Components (lib/presentation/elements/)

    • NotificationPermissionBottomSheet - Beautiful permission request UI
      • Platform-native dialogs (Cupertino on iOS, Material on Android) via adaptive_dialog
      • Full text customization for localization support
      • Handles denied state with settings prompt
    • NotificationPermissionStatusWidget - Real-time permission status display
    • NotificationPermissionBuilder - Headless builder for custom UIs
      • Provides permission status and request method via callback
      • Automatically rebuilds on status changes
      • Enables fully custom permission UIs
    • NotificationBadgeWidget - Notification count badge overlay
      • Automatic sync with NotificationService badge count
      • Manual mode for custom counts
      • Automatic overflow handling ("99+")
      • Customizable colors, size, and position
      • Optional hide-when-zero behavior
      • Configurable polling interval
    • Customizable styling and messaging
    • Automatic platform-specific behavior
  • Documentation

    • NOTIFICATION_GUIDE.md - Comprehensive usage guide with examples
    • Before/after code comparisons showing boilerplate reduction
    • Platform configuration guides
    • Permission strategy best practices

Key Benefits

  • Massive Boilerplate Reduction: ~300 lines → 1 initialize() call
  • Better Permission UX: Controlled timing, contextual prompts, recovery flows
  • Optional Feature: Zero impact on apps that don't use notifications
  • Production Ready: Error handling, logging, platform-specific optimizations
  • Framework Agnostic: Works with any navigation system (Navigator 1.0/2.0, go_router, etc.)

Dependencies Added

  • flutter_local_notifications: ^18.0.1 - Local notification display
  • app_badge_plus: ^1.1.5 - Badge count management
  • adaptive_dialog: ^2.2.0 - Platform-native dialogs (iOS/Android)

Migration

No breaking changes. This is a new optional feature. See NOTIFICATION_GUIDE.md for setup instructions.

0.1.0 #

🎉 Major Improvement: Full Web Platform Support for Real-Time Remote Config #

Background

Firebase Remote Config onConfigUpdated listener is now fully supported on web platforms as of firebase_remote_config 6.1.0! Previously, this package used polling-based workarounds (periodic refresh every 5 minutes) since the listener wasn't available on web.

Breaking Changes

  • None - This release maintains full backward compatibility

Removed/Deprecated

  • WebRemoteConfigRefreshService - Marked as @Deprecated (kept for backward compatibility, will be removed in 1.0.0)
    • No longer needed - the real-time onConfigUpdated listener now works on web
    • Periodic polling (every 5 minutes) replaced by instant real-time updates
    • File kept for backward compatibility but removed from package exports
  • webForceInitialFetch() - Removed from app_remote_config_init.dart
    • No longer needed - initial fetch and updates handled automatically by listener

Changed

  • AppVersionUpdateService - Now uses real-time listener on ALL platforms including web
    • Removed if (kIsWeb) return; check that prevented listener setup on web
    • Added platform-aware logging for better debugging
    • Web apps now receive Remote Config updates instantly (< 1 second) instead of waiting up to 5 minutes
  • app_remote_config_init.dart - Simplified initialization logic
    • Removed web-specific initialization code
    • Unified initialization flow across all platforms
    • Updated documentation to reflect real-time support on all platforms

Benefits

  • Real-Time Updates on Web: Config changes appear instantly (< 1 second vs 0-5 minutes)
  • Better Performance: No unnecessary polling - network calls only when configs change
  • Lower Bandwidth: ~98% reduction in network usage on web (~720 KB/day → ~12 KB/day per user)
  • Unified Codebase: Single code path for all platforms, easier to maintain
  • Cleaner Code: ~230 lines of workaround code removed/deprecated

Dependencies Updated

firebase_remote_config: ^6.1.0  # Now supports onConfigUpdated on web

Migration Guide

Apps using this package don't need any changes - improvements are internal! However:

  • If you were manually using WebRemoteConfigRefreshService, stop using it (deprecated)
  • If you were calling webForceInitialFetch(), remove those calls (function removed)
  • The AppVersionUpdateService now automatically handles all platforms including web

See REMOTE_CONFIG_WEB_MIGRATION.md for detailed migration information.

Files Modified

  • lib/app/helpers/app_version_update_service.dart - Enabled listener on web, updated comments
  • lib/app/helpers/app_remote_config_init.dart - Removed web workarounds, simplified logic
  • lib/app/helpers/web_remote_config_refresh_service.dart - Marked as deprecated
  • lib/dreamic.dart - Removed export of deprecated service

Documentation Added

  • REMOTE_CONFIG_WEB_MIGRATION.md - Comprehensive migration guide
  • CHANGELOG_WEB_SUPPORT.md - Detailed technical changelog

0.0.12 #

Added #

  • Robust Enum Converters - Crash-proof enum serialization that handles unknown values gracefully
    • Added RobustEnumConverter<T> base class for creating enum converters
    • Added NullableEnumConverter<T> - returns null for unknown enum values (recommended for nullable fields)
    • Added DefaultEnumConverter<T> - returns a default value for unknown enum values (recommended for non-nullable fields)
    • Added LoggingEnumConverter<T> - logs unknown values before returning default (recommended for monitoring)
    • Solves the problem of old app versions crashing when server adds new enum values
    • No need for @JsonKey(unknownEnumValue: ...) on every field
    • No need for "unknown" value in every enum
    • Forward compatible - old apps gracefully handle new server enum values

Documentation #

  • ENUM_QUICK_START.md - 5-minute quick start guide
    • Simple step-by-step instructions
    • Strategy selection guide
    • Real-world examples
    • Clear and concise format
  • ENUM_SOLUTION_ARCHITECTURE.md - Design decisions document
    • Problem analysis and solution rationale
    • Architecture decisions and trade-offs
    • Implementation structure overview
  • MODEL_SERIALIZATION_GUIDE.md - Added comprehensive "Enum Converters" section
    • Problem explanation and traditional approach issues
    • Detailed guide for each converter type
    • Real-world scenarios and use cases
    • Benefits over traditional approach
    • Migration guide and best practices
    • Troubleshooting section
  • enum_example.dart - Complete real-world example showing:
    • Multiple enum types in one application
    • Different converter strategies for different use cases
    • Service layer integration
    • Backward compatibility scenarios

Tests #

  • enum_converters_test.dart - Comprehensive test suite for enum converters
    • Tests for all three converter types (Nullable, Default, Logging)
    • Real-world backward compatibility scenarios
    • Edge cases (empty strings, whitespace, case sensitivity)
    • Multiple unknown values handling
    • Roundtrip conversion tests

0.0.11 #

Added #

  • Test Utilities - New testing support for widgets that use Dreamic components
    • Added MockAppCubit class for widget testing with configurable network, auth, and app states
    • Added initializeTappableActionForTesting() to prevent timer-related test failures
    • Added wrapWithMockAppCubit() helper for easy test setup with BlocProvider
    • Exported test utilities from main package for consumer access
    • Supports dynamic state changes during tests via setter methods

Documentation #

  • TESTING_GUIDE.md - Comprehensive testing guide with examples for:
    • Testing TappableAction widgets with various configurations
    • Testing network-dependent features and state transitions
    • Testing authentication-dependent features
    • Testing loading states and error scenarios
    • Advanced patterns including async operations, golden tests, and complex state combinations
    • Complete working examples and common issue solutions
  • DREAMIC_FEATURES_GUIDE.md - Added Testing section with quick start guide and best practices
  • SETUP_NEW_PROJECT.md - Renamed from SETUP_DREAMIC.md for clarity

0.0.10 #

Fixed #

  • Test Compatibility - Logger and AppConfigBase now handle GetIt not being initialized (e.g., in test environments)
    • Added try-catch blocks to safely handle cases where RemoteConfigRepoInt is not available
    • Logger defaults to debug level when AppConfigBase.logLevel cannot be retrieved
    • Prevents crashes when using logging utilities in unit tests without full app initialization

0.0.9 #

Changed #

  • Reduced Log Verbosity - Version checking and app lifecycle logs now use verbose level (logv) instead of debug level (logd)
    • App resume/pause lifecycle events moved to verbose logging
    • Version check details and comparisons moved to verbose logging
    • Remote Config status checks moved to verbose logging
    • Only critical events remain at debug level: actual version updates (required/recommended) and Remote Config changes
    • Significantly reduces log noise during normal app operation

0.0.8 #

Fixed #

  • Web Platform Logging - Logger now uses print() instead of debugPrint() in release mode on web to ensure logs appear in browser console (debugPrint is compiled away in release builds)
  • Firebase Crashlytics Web Support - Added platform checks to prevent runtime errors on web where Crashlytics is not supported

Changed #

  • Error Reporting Configuration - Added detailed configuration logging in appInitErrorHandling() to help verify error reporting setup
  • Firebase Initialization - Removed automatic Firebase initialization from appInitErrorHandling(). Firebase must now be initialized via appInitFirebase() first when using Crashlytics

Documentation #

  • Updated ERROR_REPORTING_GUIDE.md with Quick Start Checklist and clearer Sentry integration best practices
  • Enhanced error_reporter_example.dart with step-by-step integration examples

0.0.7 #

Documentation #

  • Updated ERROR_REPORTING_GUIDE.md and error_reporter_example.dart with improved Sentry integration examples

0.0.6 #

Added #

  • EnvironmentType Enum - Type-safe environment configuration
    • New EnvironmentType enum with five values: emulator, development, test, staging, production
    • AppConfigBase.environmentType now returns enum type instead of string
    • Added AppConfigBase.environmentTypeString convenience getter for string value
    • Includes fromString() method for parsing --dart-define values
    • Provides IDE autocomplete support and compile-time type checking
    • Exhaustive switch statement checking for better code safety
  • Centralized App Version Methods in AppConfigBase
    • getAppVersion() - Returns cached PackageInfo instance
    • getAppVersionString() - Returns version string (e.g., "1.0.0")
    • getAppBuildNumber() - Returns build number string (e.g., "42")
    • getAppRelease() - Returns formatted release string (e.g., "my-app@1.0.0+42")
    • Cached implementation for better performance
    • Works correctly on Flutter Web where PackageInfo can have issues
  • Exported AppConfigBase in main library file (dreamic.dart)
    • Makes AppConfigBase and EnvironmentType available to package users

Changed #

  • Updated Sentry Integration Documentation - Comprehensive updates across all docs
    • All examples now use Sentry's recommended appRunner pattern with SentryFlutter.init()
    • Integrated appRunIfValidVersion() in all appRunner examples for automatic version checking
    • Updated ERROR_REPORTING_GUIDE.md with three clear integration approaches:
      • Approach A: appRunner (RECOMMENDED) - Simplest, no custom ErrorReporter needed
      • Approach B: appRunner with Dreamic Config - For integration with Dreamic's configuration system
      • Approach C: Manual Integration (Advanced) - Full control with ErrorReporter interface
    • Updated DREAMIC_FEATURES_GUIDE.md with complete examples using appRunner and version checking
    • Updated error_reporter_example.dart with detailed documentation for all three approaches
    • All examples now use AppConfigBase.environmentType.value and AppConfigBase.getAppRelease()
  • Centralized Version Information - Refactored to use AppConfigBase methods
    • Updated app_version_check.dart to use AppConfigBase.getAppVersion()
    • Updated app_version_update_service.dart to use centralized version method
    • Updated app_cubit.dart to use centralized version method
    • Updated app_update_debug_widget.dart to use AppConfigBase.getAppVersionString()
    • Removed duplicate package_info_plus imports across 5 files

Fixed #

  • Environment Configuration - More flexible and type-safe
    • Environment type now supports all standard environments (emulator, dev, test, staging, prod)
    • Backward compatible with existing --dart-define=ENVIRONMENT_TYPE=value configuration
    • Smart defaults: development in debug mode, production in release mode

Documentation #

  • ERROR_REPORTING_GUIDE.md
    • Added "Build Configuration (dart-define)" section with ENVIRONMENT_TYPE documentation
    • Restructured Sentry Integration section with clear approach comparisons
    • Added examples showing appRunIfValidVersion() integration
    • Updated all code examples to use type-safe EnvironmentType enum
    • Added troubleshooting section for wrapper-based setup
  • DREAMIC_FEATURES_GUIDE.md
    • Added environment type documentation with enum examples
    • Updated all Sentry examples to show appRunner pattern
    • Added "Complete Example" sections showing recommended patterns
    • Updated build configuration examples with proper environment and release usage

Notes #

  • 100% Backward Compatible - All existing code continues to work
    • String values via --dart-define=ENVIRONMENT_TYPE=production still work exactly the same
    • Existing error reporting configurations unchanged
    • No breaking changes to any APIs
  • Migration Path - Easy upgrade from string to enum
    • Use .value property to get string value when needed
    • Use environmentTypeString getter as convenience method
    • Existing configurations work without modification

0.0.5 #

Added #

  • BaseFirestoreModel - New abstract base class for intelligent Firebase serialization
    • Context-aware serialization (Firestore, Cloud Functions, local storage)
    • Separate methods for create vs update operations (toFirestoreCreate(), toFirestoreUpdate())
    • Support for data migration with toFirestoreRaw()
    • Cloud Functions integration with toCallable()
    • Configurable timestamp field management
    • Custom post-processing hooks
  • SmartTimestampConverter - Enhanced timestamp converter supporting multiple formats
    • Handles Firestore Timestamp objects
    • Supports Cloud Functions Map format
    • Works with milliseconds and ISO strings
    • Nullable and non-nullable variants
  • ConnectionToaster - Optional network status toast notifications
    • Shows "Connecting..." toast when network connection is lost
    • Automatically dismisses when connection is restored
    • Smart behavior: doesn't show during app startup/resume by default
    • Configurable delay before showing toast (defaults to immediate)
    • Optional showOnInitialConnection flag for showing during app load
    • Integrated into AppRootWidget as opt-in feature
  • Comprehensive documentation in docs/
    • Complete usage guide with examples (MODEL_SERIALIZATION_GUIDE.md)
    • Embedded example models covering different use cases
    • Service implementations with real-world patterns (CRUD, batch operations, transactions, and more)

Changed #

  • Enhanced model_converters.dart with new Smart converters
  • Exported data models and converters in main library file
  • AppRootWidget now supports optional ConnectionToaster integration
    • Added useConnectionToaster parameter (defaults to false for backward compatibility)
    • Added showConnectionToastOnInitialConnection parameter to control toast behavior during app startup
    • Added connectionToastDelay parameter for customizing toast display timing
    • ConnectionToaster wraps entire app content at top level when enabled, ensuring toasts appear above all UI

Notes #

  • 100% Backward Compatible - All existing converters continue to work exactly as before
  • No breaking changes to existing APIs
  • New features are opt-in only
  • Both old and new approaches can coexist in the same project

0.0.4 #

  • Upgraded dependencies

0.0.3 #

  • Initial public release.