Repository navigation
Apply offer coupons from ?offer= links at the Welcome checkout - #452
Open
paulocastellano wants to merge 4 commits into
Open
paulocastellano wants to merge 4 commits into
paulocastellano wants to merge 4 commits into
Conversation
added 4 commits
October 26, 2026 20:49
An offer is a Stripe coupon tied to an account through ?offer=<slug> on any page. The Welcome plan step shows its badge and price and checks out with its coupon instead of the first-month one; the subscription.created webhook marks it redeemed. Dates and places live on the offers row only.
- Match offer slugs exactly on both engines through Offer::findAvailable() - Claim at once for a signed-in owner; keep only available slugs for guests - Resolve the offer once per request and hand it to checkout - Redeem only active or trialing subscriptions, on created and updated - Check offer copy per locale without fallback (OfferResource), in every locale - Show the offer badge only with its price - Cover Google, GitHub and register/login round trips, end to end too
The plan price stays the list price; a line under it says what the coupon takes off and until when. The payment method shows its brand logo and the brand's proper name, keeping the generic card icon for brands without one.
This branch has not been deployed
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
What
An offer is a Stripe coupon that replaces the $1 first month at the Welcome checkout. A visitor gets one by opening any app URL with
?offer=<slug>:app.trypost.it/register?offer=black-friday-2026for ads and landing pagesapp.trypost.it/login?offer=<slug>for win-back emailsWhen a visitor has an offer, the plan step shows a badge naming it ("You're seeing this price thanks to Black Friday 2026.") and the offer's price, and the checkout applies its coupon.
How
offers:slug,stripe_coupon_id,starts_at(required),ends_at(null = no end),max_redemptions(null = no limit).account_offer(pivot):claimed_at,redeemed_at.CaptureOfferweb middleware keeps the slug of an available offer in the session and claims it on the first authenticated request (ClaimOffer). This covers email signup, Google, GitHub and login without touching their controllers.ConfigureSubscriptionCheckoutapplies the offer first, then the first-month coupon, then the trial. The offer is checked again when checkout starts. Its id goes in the subscription metadatatrypost_offer_id, and thecustomer.subscription.createdwebhook writesredeemed_at(RedeemOffer).lang/*/offers.php, under the slug with dashes turned into underscores, in all 16 locales.OfferCopyTestfails when an offer row has no copy. Without its name, the badge falls back to a generic sentence, and the page never shows a raw key.?offer=links)"), including how to add an offer: create the coupon, add a data migration, add the copy, deploy.No offer ships in this PR. The first one comes with its own migration and copy.
Decisions to check
trypost_first_month_coupon_id, so PostHog does not count it as the $1 first month.checkout.startedgetsoffer_sluginstead.Alertcomponent, notBadge, becauseBadgecan't hold a sentence on a phone.Deferred (not in this PR)
ClaimOfferqueriesoffersbefore the cheaperhasActiveSubscription()check.offerFor()accepts any monthly plan. "Socials only" holds only because Welcome is the only caller.Tests
LocalizationParity,SingleLineCopy,tests/Browser/WelcomePlanTest.php(12) and eslint.Manual test checklist
Three coupons exist in the Stripe test account (delete them when done):
TRYPOST_TEST_BF_50_3MTRYPOST_TEST_WINBACK_30TRYPOST_TEST_FIRST_MONTH_FREEThe local setup is already done on Paulo's machine:
black-friday: 50% off, ends in a week, 2 places.winback: 30% off, no end.first-month-free: starts tomorrow, on purpose.lang/*/offers.php, uncommitted (English everywhere except pt-BR)..env.Restart
npm run devso the new lang keys reach the browser.New signup with an offer
/register?offer=black-fridayand sign up with email.$9.50with "for your first 3 months"4242 4242 4242 4242.account_offer.redeemed_atis set for that account.R$ 49,50).Moving between register and login keeps the offer
/register?offer=black-friday→ "Log in" → "Sign up" → sign up: the offer is kept./login?offer=winback→ "Sign up" → "Log in" → log in: the offer is kept.What must NOT give an offer
/register?offer=does-not-exist: no badge, normal$1first month./register?offer=first-month-free(starts tomorrow): no badge.starts_atto the past and sign in: still no badge, because nothing is kept for later./register?offer=BLACK-FRIDAY(wrong case): no badge./calendar?offer=winback: no row inaccount_offer.Win-back (former customer)
/login?offer=winbackand log in.$13.30"for 6 months". Checkout applies 30% off.Limits and timing
black-fridayhasmax_redemptions = 2. After two paid checkouts, a third new signup with the link gets no badge.ends_atto the past, then click "Activate": checkout shows the$1first month, not the offer.Cleanup
php artisan tinker --execute 'App\Models\Offer::whereIn("slug", ["black-friday","winback","first-month-free"])->delete();'git checkout lang/*/offers.phpSTRIPE_WEBHOOK_SECRET/WEBHOOK_URLin.env.