Routes Flutter framework errors (FlutterError.onError) and uncaught
async/platform errors (PlatformDispatcher.onError) to Crashlytics, but
only in release builds — debug keeps its red error screens and console
traces, and collection is disabled via setCrashlyticsCollectionEnabled
(kReleaseMode) so development crashes never reach the dashboard. Adds
the Crashlytics Gradle plugin alongside the existing google-services
FlutterFire config. iOS dSYM upload run script still to be added in
Xcode (symbolication only; crash capture works without it).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds an in-app review prompt gated by ReviewPromptPolicy: only after a
3-star stage win, once the player has cleared >=5 stages, at most once
ever (persisted reviewRequested flag). ReviewService swallows all
failures and only burns the one-shot when the store actually shows the
sheet, so an unavailable store retries on a later win. StoreReviewer
wraps in_app_review behind a Reviewer seam so unit tests skip platform
channels. 13 new tests; full suite 194 green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The developer is an individual without a Korean business registration, so
the App Store / Play paid-apps (merchant) agreements can't be completed.
Hide the Remove Ads + Restore tiles and skip IAP init; ads always show.
AdMob revenue is independent of those agreements. Reversible: flip
kIapEnabled to re-enable once a merchant agreement exists. Bump to build 2;
drop the now-unused IAP review-screenshot generator.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The '(build 4)' marker was a device build-delivery debugging aid, now
resolved. Public release shows a clean version string.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Generator: raise the early bot-win-rate floor to ~0.97 (near-guaranteed first
stages), widen the early move-buffer margin, and delay score-chase/line-sprint
objectives until stage 9/13 so the opening stretch is pure clear-a-gem. Result:
stage 1 now 16 moves (was 11), stages 1-9 all simple gems at 90-99% bot win.
Preserves theme.bgm=season_001; manifest SHA regenerated. Settings tag -> build 4.
- activeSeason now returns the first season (Season 1 'First Bloom') so a new
player starts at spring instead of the newest season.
- Bundle the owner-picked CC0 tracks menu.mp3 + season_001.mp3 (BGM now audible).
- Settings footer shows 'v1.0.0 (build 3)' so test builds are identifiable.
180 tests green, analyze clean.
MusicService (looping audioplayers player, independent of SFX) driven by the
active season theme's new 'bgm' key; switches track on season change, pauses on
app background, all failures swallowed. Separate Music on/off toggle in Settings
(persisted, independent of SFX). Season packs carry bgm keys (menu/season_001/
season_002), manifest regenerated. Assets slot assets/audio/bgm/ ready — drop in
menu.mp3/season_001.mp3/season_002.mp3 (CC0) and it plays; silent until then.
180 tests green, analyze clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
On device the floating top-left X (48px IconButton) overlapped the HUD moves
chip. Move it into the column flow above the HUD, left-aligned, so it never
collides. Found via on-device test build.
Replaces the TODO_REAL_* ad-unit placeholders with the owner's real AdMob
console ids (publisher pub-5605900229781491) and the native AdMob app ids
(iOS ~8397095848, Android ~8257495040) in place of the Google sample ids.
Debug builds still serve Google test ads via kDebugMode; release builds now
use the real units.
The flutter_test default font draws every glyph as a box, so the wordmark was
unreadable. Load the OFL Titan One TTF via FontLoader in the generator and
render 'Block Seasons' + tagline with it; fit the text within the 1024 width.
Font used only by the asset generator, not bundled in the app.
Adds PressableScale (0.94 squish on tap-down) around the Adventure
FilledButton, Classic OutlinedButton, and each season-map stage node.
Replaces all in-app MaterialPageRoute pushes with a gentle fade+scale
PageRouteBuilder (320ms in, 240ms out) via a new fadeRoute helper.
Introduces AppIconMark (navy gradient field + 2×2 glossy blocks reusing
paintGlossyTile) and a testWidgets generator that writes icon.png,
icon_background.png, and icon_foreground.png to assets/icon/ for the
upcoming flutter_launcher_icons task.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
iapServiceProvider was lazy and only read by Settings, so the purchase stream
attached only after opening Settings — deferred/interrupted/restored
transactions wouldn't be delivered or completed until then. Read it in the
post-frame callback so the stream is live for the whole session.
On a cold start the consent flow is still running when home first builds, so
createBanner returns null and the slot stayed empty until a rebuild. AdService
now exposes an isReady ValueNotifier; BannerAdSlot listens and retries its load
once MobileAds finishes initializing. Verified: analyze clean, 169 tests green.
setAnalyticsCollectionEnabled(kReleaseMode) after Firebase init so the native
SDK's automatic events (session_start, screen_view) stay out of production
analytics in debug too — not only our custom events. Verified on simulator:
release builds collect, debug builds do not.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
flutterfire configure registered the iOS/Android apps under project
block-seasons and generated firebase_options.dart + native config. main()
now initializes Firebase and routes analytics through FirebaseAnalyticsBackend
in release builds (console logger in debug, so dev traffic never pollutes
GA4). Firebase init is guarded — failure falls back to the debug logger
rather than blocking startup.
firebase.json keeps the existing Hosting config and gains the FlutterFire
platform section. Client config files are committed (they ship in the binary;
Firebase security is enforced by rules, not config secrecy).
flutter analyze clean, all 161 tests green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds firebase_core + firebase_analytics and a FirebaseAnalyticsBackend that
adapts the existing AnalyticsBackend interface to GA4. Kept in its own file
so the typed AnalyticsService and DebugAnalyticsBackend stay free of the
firebase dependency (unit tests never pull in platform channels).
Not yet wired into main() — that needs lib/firebase_options.dart from
flutterfire configure (owner step). All 161 tests still green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
seasonsProvider now watches seasonRefreshProvider instead of relying on
a HomeScreen ref.listen, making the refresh ordering-independent. Removes
the redundant listener from HomeScreen. Appends *.pid to .gitignore.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Expose engine.rescueUsed getter and surface it through GameViewState so
the result overlay can omit the watch-ad FilledButton after a rescue has
been consumed, preventing a second tap from calling useContinue /
addExtraMoves and hitting their StateError guard. Give-up is promoted to
FilledButton when rescue is unavailable for clear affordance.
Also emit stageStart / endlessStart analytics in restart() so every
attempt (not just the first) is bracketed by a matching start event.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Prevents StateError when data builder is called with empty list
by displaying loading indicator instead of passing empty list to
activeSeason(), matching title screen behavior.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add seasonRefreshProvider (once-per-session FutureProvider) and activeSeason()
helper; HomeScreen listens and invalidates seasonsProvider when new packs arrive;
season_map_screen and season_title_screen switch from list.first to activeSeason.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Extends ContentRepository with optional cacheDir: cached packs in
<cacheDir>/seasons/*/pack.json merge with bundled ones (cached wins
for same id), corrupt/future-schema packs silently ignored, refresh()
fires ContentDownloader.sync() once per session. main() wires the real
cache+downloader instance; default constructor stays bundled-only for tests.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Wrap AnimatedBuilder in SizedBox.expand so the Stack fills the full
Scaffold body; alignment: Alignment.center now centers within the whole
screen instead of within the wordmark-sized intrinsic box.
Adds a regression widget test (test/ui/splash_screen_test.dart) that
asserts the wordmark dx is within 1px of screen-center at 1500ms into
the animation.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add SeasonFlowNotifier.clear() to null out the flow state
- Call clear() in HomeScreen's Classic button before startStage()
- Broaden tutorial-end guard to next.phase != GamePhase.playing (covers stuck)
- Add regression test: clear() resets flow to null
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>