Build a Subscription Tracker with FlutterGo, Then Fix the Renewal That Moved to Sunday
We built a subscription tracker with FlutterGo from one prompt, tested its renewal dates, then fixed a clock-change bug that moved Monday renewals to Sunday.

We built Renewly, a subscription tracker with a monthly total, renewal dates and a "renewing soon" banner, from one prompt in the FlutterGo studio. The studio's agent run took 24 minutes 17 seconds. Everything we tested in the preview worked, including the month-end dates that subscription apps usually get wrong. Reading the generated Dart then turned up two bugs that only appear in countries that change their clocks, and one fix prompt cleared both in 58 seconds.
This walkthrough has the exact prompts, what we tested and what we didn't, both bugs with the code behind them, and the Dart check you can rerun yourself.
What you need
- A FlutterGo account. We built this on 11 October 2026.
- About 30 minutes, most of it waiting for the build.
- Optional: the Dart SDK, if you want to rerun our date check. We used Dart 3.13.5.
The generated project uses flutter_riverpod 2.6.1 for state, go_router 14.6.2 for navigation and shared_preferences 2.5.3 for storage, with sdk: ^3.12.2 in its pubspec.yaml. That's what this build produced. Another build from the same prompt could differ.
Step 1: Describe the app
This is the prompt we pasted into the studio. The prompt box turned each -> into an arrow; otherwise it's unchanged:
Build a subscription tracker app called Renewly. Users add subscriptions with a name, a price, a billing cycle (weekly, monthly, quarterly or yearly), the first billing date and an optional category (Streaming, Software, Fitness, News, Other). One currency for the whole app, picked in Settings (default USD).
Home screen: the total monthly cost at the top, where every cycle is converted to a monthly equivalent (weekly x 52 / 12, quarterly / 3, yearly / 12). Below it, a list sorted by next renewal date showing name, price, cycle and "renews in N days" ("renews today" when it is today). A category filter, and a "Renewing in the next 7 days" banner when anything is due soon.
The next renewal date is computed from the first billing date by stepping whole cycles until the date is today or later. For monthly, quarterly and yearly cycles keep the original day of the month and clamp to the last day of shorter months (Jan 31 -> Feb 28 or 29 -> Mar 31; Feb 29 yearly -> Feb 28 in non-leap years).
Detail screen: edit, delete with a confirmation, the next 6 renewal dates and the yearly cost.
Store everything locally so it survives a restart. Seed 4 example subscriptions on first launch. Clean Material 3 design that works in light and dark mode.
The third paragraph does the heavy lifting. "Add one month" is ambiguous on the 31st, and if you don't say what should happen, a generator has to guess. Spelling out "keep the original day and clamp" with an example gives it a rule it can test against. The weekly conversion is spelled out for the same reason: 52 weeks divided by 12 isn't the same as "4 weeks a month".
Step 2: First run
The studio reported its run done after 24 minutes 17 seconds. Its checklist said 5 of 7 planned screens were built and listed the Add and Edit screens as open, but the code for both was already in subscription_form_screen.dart and both screens worked in the preview, so we didn't run the extra builds. We tested in the static preview page, which shows the app in an iPhone 16 frame.

The app opened with a three-page onboarding and four sample subscriptions (NovaStream, PixelForge Pro, Pulse Gym and Daily Ledger), as the prompt asked. The seeds are placed relative to today, so one of them always renews today and another inside the week, which shows the banner on first launch. Unlike our packing list build, the onboarding didn't come back after a reload.
Step 3: Test against the prompt
Here's what we actually ran in the preview. Today was Sunday 11 October 2026 on the test machine, which uses Pakistan time (no daylight saving).
| Check | Result |
|---|---|
| Seeded monthly total | $100.57, which matches 49 + 15.99 + 29 + 79 ÷ 12 |
| Pulse Gym, first billed today | "renews today" |
| Add Cloud Drive, $9.99 monthly, first billed Sat 31 Jan 2026 | "renews in 20 days"; total $110.56 |
| Cloud Drive's next 6 renewals | Oct 31, Nov 30, Dec 31, Jan 31, Feb 28, Mar 31 (correct clamping) |
| Cloud Drive's yearly cost | $119.88 |
| Filter: Software | Only Cloud Drive and PixelForge Pro |
| Reload the preview | Subscriptions kept, onboarding not shown again |
| Delete Cloud Drive | Confirmation dialog, then gone; total back to $100.57 |
| Add Meal Kit, $12 weekly, first billed Mon 5 Oct | Form shows $52.00 a month; "renews in 1 day"; banner says 3; total $152.57 |
| Meal Kit's next 6 renewals and yearly cost | Oct 12, 19, 26, Nov 2, 9, 16; $624.00 |
| Settings: switch to EUR | € on every price, and it survived the rebuild after the fix |

Two small things we noticed. The monthly total stays at the all-subscriptions figure when a category filter is on, which is a fair choice but worth deciding on purpose. And the "Other" category uses the same stock portrait as the home header, so Meal Kit's detail page shows a woman in headphones.
Not tested: editing a subscription, the quarterly cycle, a yearly subscription started on 29 February (we checked that one in Dart instead, below), an empty list, form validation, dark mode, large text sizes, and real Android or iOS devices.
Step 4: Read the date code
Testing in one time zone can't catch clock-change bugs, so we read lib/core/billing_math.dart. The month logic is careful:
static DateTime addMonthsKeepingDay(
DateTime from,
int months,
int originalDay,
) {
var year = from.year;
var month = from.month + months;
while (month > 12) {
month -= 12;
year += 1;
}
while (month < 1) {
month += 12;
year -= 1;
}
final lastDay = DateTime(year, month + 1, 0).day;
final day = originalDay > lastDay ? lastDay : originalDay;
return DateTime(year, month, day);
}
DateTime(year, month + 1, 0) is the last day of month, and the clamp uses originalDay (the day from the first billing date), not the day of the previous result. That second detail is what stops a subscription from sticking at the 28th forever after February. It's the most common mistake in renewal code, and this build avoided it.
The weekly step and the day count are where it goes wrong:
BillingCycle.weekly => from.add(const Duration(days: 7)),
static int daysUntilRenewal(Subscription sub, [DateTime? now]) {
final today = dateOnly(now ?? DateTime.now());
final next = nextRenewalDate(sub, now);
return next.difference(today).inDays;
}
Duration(days: 7) is 168 hours, not "the same time next week". Dart's docs for DateTime.add warn that when the daylight saving offset changes, the result "won't have the same time-of-day" and "may not even hit the calendar date" you expect. The docs for DateTime.difference say the same from the other side: between two local midnights the difference "may not be a multiple of 24 hours", and inDays drops the leftover hours.
Step 5: Prove it with a time zone
We copied billing_math.dart and the two model files it imports into a small Dart project (we replaced the category enum with a stub without its image paths, since the math doesn't use it) and ran a script with the TZ environment variable set to three time zones. UK clocks go back on 25 October 2026 and forward on 28 March 2027 (GOV.UK). US clocks go back on 1 November 2026 (NIST).

The results, with the generated code (before the fix):
| Case | London | New York | Karachi |
|---|---|---|---|
| Weekly from Mon 19 Oct 2026: second renewal | Sun 25 Oct, 23:00 | Mon 26 Oct | Mon 26 Oct |
| Same subscription on Sun 1 Nov: label | "renews today" | "renews in 1 day" | "renews in 1 day" |
| Monthly on the 5th, checked on 20 Mar 2027 | "renews in 15 days" | "renews in 16 days" | "renews in 16 days" |
| Monthly from 31 Jan 2027: next 6 | correct | correct | correct |
| Yearly from 29 Feb 2024 | Feb 28 2025, 2026, 2027, then Feb 29 2028 | same | same |
So for a user in London, a Monday weekly subscription flips to Sunday from late October and stays there, the "renews today" label and the banner fire a day early, and in late March a renewal 16 calendar days away shows as 15. In New York the weekly drift starts after 1 November instead. In Karachi, where we tested the UI, nothing is wrong, which is exactly why the preview looked fine.
The March case comes out right in New York only because US clocks have already changed by 20 March. Check a few days before 14 March 2027 and it's off there too.
Step 6: One fix prompt
We described both symptoms and the rule we wanted, and asked it to leave the month logic alone:
Two date bugs in lib/core/billing_math.dart. They only show up in time zones with daylight saving time (London, New York), so the preview here looks fine.
1) Weekly renewals use from.add(const Duration(days: 7)) on a local-midnight DateTime. Across the autumn clock change that lands at 23:00 on the previous day, so in London a Monday renewal shows up as Sunday from 25 October onwards.
2) daysUntilRenewal uses next.difference(today).inDays. Across the spring clock change one day is only 23 hours, so "renews in 16 days" shows as "renews in 15 days".
Step dates and count days in whole calendar days, not hours: for example DateTime(y, m, d + 7) for weekly, and count days between DateTime.utc(...) values built from the year, month and day. Keep the month-end clamping exactly as it is and don't change anything else.
The run finished in 58 seconds. These are the two changes it made, with its own comments:
// Calendar days, not 168 hours — Duration(days: 7) drifts across DST.
BillingCycle.weekly => DateTime(from.year, from.month, from.day + 7),
/// Whole calendar days between two dates (DST-safe via UTC midnight).
static int calendarDaysBetween(DateTime from, DateTime to) {
final a = DateTime.utc(from.year, from.month, from.day);
final b = DateTime.utc(to.year, to.month, to.day);
return b.difference(a).inDays;
}
static int daysUntilRenewal(Subscription sub, [DateTime? now]) {
final today = dateOnly(now ?? DateTime.now());
final next = nextRenewalDate(sub, now);
return calendarDaysBetween(today, next);
}
DateTime(y, m, d + 7) asks for a calendar date and lets Dart roll the day over into the next month, so it always lands on local midnight. UTC has no daylight saving, so two UTC midnights are always a whole number of days apart.
We applied the same two changes to our copy and reran the script. All three time zones now agree: the weekly dates are Mon 26 Oct, Nov 2, 9, 16 and 23 at midnight, Sunday 1 November says "renews in 1 day", and the March check says 16 days. The month-end and 29 February results didn't change. dart analyze reported no issues on the copied files. In the app, the rebuilt preview kept our data and currency and showed the same weekly dates as before, which is what we'd expect in Karachi.
We didn't run the UI in a London or New York time zone. The function-level runs are what we're reporting. The script and both outputs are short enough to rerun: set TZ=Europe/London and call nextRenewalDates on a weekly subscription that starts the Monday before 25 October.
What the generated code got right
- Month-end clamping, as above, including returning to the 31st after February.
- Leap days: a yearly subscription from 29 February 2024 renews on 28 February in 2025, 2026 and 2027, then on 29 February 2028.
- The totals: weekly × 52 ÷ 12, quarterly ÷ 3 and yearly ÷ 12, exactly as asked, and a yearly cost on the detail page.
- Structure: billing math lives in one pure Dart class (
BillingMath) that takes an optionalnow, which is what made it easy to test in isolation. Storage is a single JSON list inshared_preferences, and Riverpod providers sort and filter it.
What we'd check next
- Run the untested rows above, especially editing a subscription and a quarterly cycle.
- Add unit tests for
BillingMaththat pass a fixednowand run under a DST time zone in CI (for exampleTZ=Europe/London flutter test), so neither bug can come back quietly. - Decide whether the sample subscriptions and onboarding belong in a release build.
- Replace the stock portrait on the "Other" category.
This is the third date bug in our tutorial series, after the warranty tracker's expiry date and the packing list's night count. Date arithmetic is the first thing we read in any generated app, and a TZ=Europe/London run is the cheapest test we know.
If you want to try this yourself, FlutterGo is an AI Flutter app builder: paste the prompt above, run the same checks, and read the date code before you trust it. For the state layer this build used, see our Flutter state management guide, and for where the data lives, the Flutter local database guide.

