You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
feat: free trial and store intro offer on the billing screen - #176
Shows a free trial on the billing screen and discloses a store introductory offer only to a customer who may take it.
What changes
MagicStarterProduct.trialDays: decoded from each plans product row's trial_days (magic-starter-laravel 0.0.23 answers it per caller, 0 when the caller is ineligible).
MagicStarterBillingController.trialEnd: trialEndsAt ?? currentPeriodEnd, only while the entitlement is trialing (a store trial carries no trial_ends_at).
Billing screen:
Current plan card: a "Trial" badge, and a trial line in place of the renewal line: "Free trial ends (N days left), then per ", the store sentence on a store trial, and "Free trial ends . It will not renew." for a cancelled trial.
Web plan card for a product with trialDays > 0: "Free for N days. Card required, then per ." above the button, and the button reads "Start free trial". Checkout is unchanged.
Store disclosure: "Free for , then per " only when StoreProductOffer.introEligible is true and the intro price is 0. A paid intro keeps the plain price line (the wire has no cycle count, so a period alone would understate a multi-period offer).
The billed price stays the large figure in every case (App Store guideline: the billed amount is the most prominent pricing element).
New en.stub keys (listed in the CHANGELOG and doc/basics/teams.md) for hosts that publish en.json.
Requires magic_payments ^0.0.9 (introEligible) and magic-starter-laravel 0.0.23 for trial_days (an older backend reads as no trial).
Verification
flutter analyze: no issues. flutter test: 1925 pass (trial and intro states at 390 and 1440 px, days-left plurals, cancelled trial, ineligible and paid intros, long plan name with both badges).
Kodizm (AI-generated). May contain mistakes; verify before acting.
Looks correct to merge: the trial and intro-offer copy only shows behind the right gates, and checkout behaviour is unchanged.
I read the controller, model, view, pubspec, en.stub and fixture diffs, and checked them against callers and against CLAUDE.md and .claude/rules/views.md. Here is why I think it is correct:
trialEnd only returns a value while planStatus == trialing. _trialEndsAt is cleared on reset and set from the same entitlement read as the snapshot, so a stale date cannot show up after the trial converts.
_webTrial uses the same gates as _selectPlan's web branch (storeRail == null, _canPurchaseViaWeb, _saleProduct). It sits inside the _cycleOverride builder, so the line and the CTA label follow the cycle toggle while onPressed stays the same. The floor and custom tiers have no sale product, so they never get trial copy.
The store intro line needs introEligible, a single-unit ISO period and introPrice == 0. Any other case falls back to the existing price sentence.
A cancelled trial (renews == false) is handled first, so it never names a price or the store renewal sentence. The days-left count rounds up and is clamped at zero.
Every new trans key is in en.stub. common.unknown already exists. The CHANGELOG.md and doc/ updates required by CLAUDE.md's post-change checklist are both in the diff.
I could not inspect magic_payments 0.0.9 (StoreProductOffer.introEligible / introPrice / introPeriod) locally. CI's "Published graph" job resolving and analysing against pub.dev is the evidence that those members exist with the types used here.
Tests
Controller tests cover trialEnd, including the fallback to currentPeriodEnd and the non-trialing case. Model and wire fixture tests cover trial_days decoding. View tests cover the trial, cancelled-trial, store-trial, eligible/ineligible/paid-intro and plural states at both widths.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Shows a free trial on the billing screen and discloses a store introductory offer only to a customer who may take it.
What changes
MagicStarterProduct.trialDays: decoded from each plans product row'strial_days(magic-starter-laravel 0.0.23 answers it per caller, 0 when the caller is ineligible).MagicStarterBillingController.trialEnd:trialEndsAt ?? currentPeriodEnd, only while the entitlement istrialing(a store trial carries notrial_ends_at).trialDays > 0: "Free for N days. Card required, then per ." above the button, and the button reads "Start free trial". Checkout is unchanged.StoreProductOffer.introEligibleis true and the intro price is 0. A paid intro keeps the plain price line (the wire has no cycle count, so a period alone would understate a multi-period offer).en.stubkeys (listed in the CHANGELOG anddoc/basics/teams.md) for hosts that publishen.json.magic_payments ^0.0.9(introEligible) and magic-starter-laravel 0.0.23 fortrial_days(an older backend reads as no trial).Verification
flutter analyze: no issues.flutter test: 1925 pass (trial and intro states at 390 and 1440 px, days-left plurals, cancelled trial, ineligible and paid intros, long plan name with both badges).dart format --set-exit-if-changed: clean.