feat: server-authoritative commerce presentation and moderation (w4b) - #5321
Conversation
Add pure purchase presentation/status helpers and moderation type constants shared by web and mobile. Wire ./commerce and ./moderation package exports.
…ight Add getPurchasePresentation and preflightPurchase procedures that decide purchase UI by platform, storefront, product, and program. Enforce native-IAP authz on completeAppStorePurchase and reject mobile platforms on createCheckoutSession. Update callers and add universal-link null-path tests.
…stants Add preflight tests for ended-sub allow and live Play reject. Move Kilo Pass copy strings into app-shared so web and mobile share one source.
Wrap completeStoreKiloPassPurchase in admit/settle with domain purchase, intent complete_store_purchase, and reconcile-first taxonomy. Settle failed on provider/user mismatch, mark reconcile-pending otherwise. Emit purchase_settled outbox event.
Map roast configs to balanced guidance at prompt time. Offer only strict, balanced, and lenient in the mobile and web pickers. Keep roast in Zod and templates for old saved configs.
Add an idempotent app:kilo-pass-stripe seed that creates a user with a real Stripe test subscription for Android web-management E2E.
…owner Gate the Kilo Pass screen on getPurchasePresentation and mount one native IAP owner only for iOS native_iap. Drive the Android card from presentation kind. Key the product cache by account and clear it on authEpoch.
Add tests for the Apple notification mismatch catch (processed_at + processed true) and the duplicate_settled failed-row replay that throws the stored domain error.
Throw on missing price id or non-test secret before any destructive cleanup. Persist the created subscription id idempotently so the Stripe webhook write does not collide.
…s gate Add content_moderation_reports, user_moderation_blocks/ mutes, and user_terms_acceptances tables with a moderation router. Gate PR UGC writes on the current Terms version. Extend softDeleteUser.
Seed a current-version Terms acceptance for UGC mutation tests and add a test proving a missing acceptance throws PRECONDITION_FAILED terms_required with no ledger row.
Add a Report AI response action on assistant messages that submits a moderation report with surface ai_output and no body, with submitted/retryable/terminal outcomes.
Add report-content, report-user, mute, and block to the comment overflow. Filter hidden users. Gate reply, comment, and submit on the current Terms version with a 13+ sheet.
Show a Retry action that resubmits the minimized payload on retryable failures and no Retry on terminal failures, with tests for both.
Show a retryable message and Retry CTA on Accept Terms and report/mute/block failures, a terminal message on outdated Terms, and add four-state tests.
Catch a transient preflight rejection and show a retryable message with a Try again CTA. Show a non-retryable state and disable the purchase tiles when preflight returns allowed: false. Add screen coverage for happy, retryable, non-retryable, and empty states.
Code Review SummaryStatus: No Issues Found | Recommendation: Merge Executive SummaryIncremental review of Files Reviewed (10 files)
Previous Review Summaries (3 snapshots, latest commit 26fd417)Current summary above is authoritative. Previous snapshots are kept for context only. Previous review (commit 26fd417)Status: No Issues Found | Recommendation: Merge Executive SummaryIncremental review of Files Reviewed (9 files)
Previous review (commit 9da7842)Status: No Issues Found | Recommendation: Merge Executive SummaryIncremental review of the 5 fix commits since the previous review (4af76fc → 9da7842): all 13 previously reported findings are resolved — the Terms-gate Alerts are now non-cancelable with an Files Reviewed (23 files)
Review notes: verified no remaining callers of the removed Previous review (commit 4af76fc)Status: 13 Issues Found | Recommendation: Address before merge Executive SummaryThe highest-risk issues cluster in the new server-authoritative purchase flow: the mobile IAP owner's recovery SKUs now depend on the StoreKit product fetch (charged-but-uncompleted transactions may never be recovered when that fetch fails), the post-purchase invalidation misses Overview
Issue Details (click to expand)WARNING
SUGGESTION
Files Reviewed (65 files)
Review notes: the DB migration (0217) is additive on new tables with correct index coverage; the kilocode-landing content SEO rules don't apply (no landing/markdown pages changed); no .md files changed, so the markdown-image rule is not triggered. Memory-leak focus areas were the Terms-gate Alert lifecycle and the IAP owner/preflight async continuations. Reviewed by grok-4.6 · Input: 73.9K · Output: 15.6K · Cached: 621.2K Review guidance: REVIEW.md from base branch |
Extract readTrpcErrorField and isTerminalTrpcCode into one shared module, drop the unreachable status-class branches, and remove the single-caller ensureTermsAccepted wrapper.
…reason Compare and store block/mute logins lowercase so case variants cannot bypass the self-check or create duplicate rows. Drop the appeal reason from the input contract since it was never persisted. Narrow the Terms gate comment to the comment-creating mutations.
Catch a settle failure after the purchase commits so it never reports a false negative, and stop leaking operation_in_progress machine text to the mobile client.
Fall back to the server product list when the StoreKit fetch is empty so recovery still runs, invalidate getPurchasePresentation after completion, and stop the purchase after an unmounted modal or a double preflight.
Make the Terms alerts non-cancelable so the gate promise always settles, handle the outdated outcome in the composer, and match the server's lowercase login normalization in self-disable and hidden-user filtering.
Delete any row matching either unique key before a plain insert so the Stripe webhook race cannot raise unique_violation, and remove the unused REVIEW_STYLES re-export that failed check-unused.
jest.fn in @jest/globals 30 takes a FunctionLike, not a return type; cast the requireActual spreads to object and type the mock as a function.
Report an unread Terms status as `unknown` instead of `accepted`. The reactive `terms_required` path now keeps a retryable error, so a failed lookup no longer clears a rejected reply, comment, or review. A write is still attempted on `unknown`; the server enforces the Terms. Block the Kilo Pass tiles when the App Store account already owns a pass on another Kilo account. The native IAP owner computes the ownership preflight, so the route keeps one `useIAP` call site.
…deration-5698 # Conflicts: # packages/db/src/migrations/meta/0217_snapshot.json # packages/db/src/migrations/meta/_journal.json
Offer roast again in the web and mobile pickers. Generate roast guidance, comment format, and summary format for roast configs.
Summary
The app now decides which purchase UI each platform may show, and the server is the source of truth for that decision.
Purchase presentation and mutations are server-authoritative, keyed by platform, storefront, product, and program. Moderation is a new router backed by four new tables.
Commerce — server-authoritative purchase presentation
A new shared helper resolves which purchase UI a platform, storefront, and product may show, and the web router exposes it plus a preflight check and a hardened completion mutation. The single allowed native-IAP combination is iOS + App Store + Kilo Pass; everything else is unavailable or web-managed, and the checkout path now refuses mobile platforms.
Files
packages/app-shared/src/commerce/purchase-presentation.ts— new pure helper:resolvePurchasePresentation,mapKiloPassStatusToClass,isNativeIapMutationAllowed, and the exact copy constants.packages/app-shared/src/commerce/index.ts— new barrel export.apps/web/src/lib/kilo-pass/purchase-presentation.ts— new wrapper:buildPurchasePresentationandgetPurchasePresentationForUserderivestatusClassand the absolutewebUrl.apps/web/src/routers/kilo-pass-router.ts— addsgetPurchasePresentationandpreflightPurchase; reworkscompleteAppStorePurchaseto take platform/storefront/product and reject non-native_iapwithFORBIDDEN commerce_not_available;createCheckoutSessionrejectsios/android;mapAppStoreCompletionErrormapsCONFLICTto a "still being processed" message.packages/app-shared/package.json— registers the./commerceand./moderationexport paths.Commerce — store completion on the operation ledger
App Store purchase completion now runs through the shared operation ledger, so a retried completion replays its settled result instead of re-executing, and a permanent provider/user mismatch settles
failedand is never retried. Apurchase_settledterminal event is emitted from the settle path.Files
apps/web/src/lib/kilo-pass/store-subscription-completion.ts— admits apurchase/complete_store_purchaserow, replays settled rows, settlesfailedon mismatch, and emitspurchase_settled.apps/web/src/lib/kilo-pass/apple-store-notifications.ts— marks a notification event processed (never retried) when completion settles a permanent mismatch.packages/db/src/operation-ledger.ts— addspurchasetoOPERATION_DOMAINS.packages/app-shared/src/analytics/event-map.ts— addsPURCHASE_SETTLED_EVENTwith a strict schema.Commerce — mobile Kilo Pass IAP
The Kilo Pass screen now gates on the server presentation, and a single IAP owner is mounted only when the presentation is
native_iapon iOS, so Android never initializes StoreKit. The profile card derives its state from the same presentation.Files
apps/mobile/src/components/kilo-pass/kilo-pass-native-iap-owner.tsx— new singleuseIAPcall site owning purchase, restore, and recovery; exposesuseKiloPassNativeIap.apps/mobile/src/lib/kilo-pass/use-store-kilo-pass-purchase.ts— purchase/restore/recovery actions, deduped toasts, inline-error ownership, and thecompleteAppStorePurchasecall.apps/mobile/src/components/kilo-pass/kilo-pass-subscription-screen.tsx— gates ongetPurchasePresentation; runspreflightPurchasebefore purchase.apps/mobile/src/components/kilo-pass/kilo-pass-subscription-card.tsx— derives the profile card from the presentation and subscription state.apps/mobile/src/lib/kilo-pass/subscription-card-state.ts— card content and ownership-preflight derivation from presentation and status class.apps/mobile/src/lib/kilo-pass/use-store-kilo-pass-products.ts— fetches App Store SKUs with a bounded connection timeout.apps/mobile/src/components/kilo-pass/kilo-pass-ios-manage.ts— new lazyexpo-iapmanagement entry point (open App Store management, dev refund).apps/mobile/src/components/kilo-pass/restore-purchases-button.tsx— reads from the IAP owner context.apps/mobile/src/app/(app)/_layout.tsx— removes the app-wideStoreKiloPassPurchaseProvider.apps/mobile/src/components/profile-screen.tsx— removes the standalone restore button.Moderation — router and tables
A new moderation router exposes report, appeal, block, mute, hidden-user, and Terms procedures, backed by four new tables. Report context is minimized by construction so a message or comment body can never be persisted.
Files
apps/web/src/routers/moderation-router.ts— new router:reportContent,reportUser,getReportReceipt,appealReport,blockUser/unblockUser,muteUser/unmuteUser,listHiddenUsers,getTermsStatus,acceptTerms; report context is.strict()with onlyplatform/storefront.packages/app-shared/src/moderation/types.ts— new shared constants: surfaces, reasons,CURRENT_UGC_TERMS_VERSION,UGC_AGE_POSTURE.packages/app-shared/src/moderation/index.ts— new barrel export.packages/db/src/schema.ts— addscontent_moderation_reports,user_moderation_blocks,user_moderation_mutes,user_terms_acceptances.packages/db/src/migrations/0219_supreme_firelord.sql— the generated migration for those four tables.apps/web/src/routers/root-router.tsandpackages/trpc/src/mobile.ts— registermoderation.apps/web/src/lib/user/index.ts—softDeleteUserdeletes the four moderation tables.Moderation — UGC Terms gate
The three comment-creating PR mutations require an accepted current Terms record before any ledger row is created, throwing
PRECONDITION_FAILED terms_requiredwhen absent.Files
apps/web/src/routers/github-pr-review-router.ts— addsassertTermsAcceptedbeforecreateReviewComment,replyToComment, andsubmitReview.Moderation — mobile UI
The mobile app adds the Report AI response control, the PR-discussion overflow menu (report content, report user, mute, block), hidden-author filtering, and the Terms gate across the reply and composer surfaces.
Files
apps/mobile/src/components/agents/report-ai-response.ts— new pure helper building the minimized report input and the retryable/terminal error toast.apps/mobile/src/components/agents/message-details-sheet.tsx— adds the "Report AI response" control.apps/mobile/src/components/pr-review/discussion/comment-row.tsx— adds the overflow menu with report/mute/block, gating self-target.apps/mobile/src/components/pr-review/discussion/pr-review-discussion-list.tsx— filters hidden authors and passes the viewer login.apps/mobile/src/components/pr-review/discussion/discussion-thread.tsx— passesviewerLoginthrough.apps/mobile/src/components/pr-review/discussion/reply-input.tsx— adds the Terms gate before replying.apps/mobile/src/components/pr-review/composer-inline-error.tsx— new inline-error component and hook with the Terms gate.apps/mobile/src/components/pr-review/pr-review-comment-composer.tsxandpr-review-submit.tsx— surface the Terms gate and inline error states.apps/mobile/src/components/pr-review/review-event-chips.tsx— new review-event radio chips extracted for the submit sheet.apps/mobile/src/lib/pr-review/classify-pr-review-query-state.ts— adds theterms-requiredmutation classification.apps/mobile/src/lib/trpc-error.ts— new sharedreadTrpcErrorFieldandisTerminalTrpcCodehelpers.Dev seed
A new dev seed creates a Stripe test-mode Kilo Pass subscription for the Android web-management E2E scenario.
Files
dev/seed/app/kilo-pass-stripe.ts— new idempotent seed creating a web-managed (Stripe) Kilo Pass subscription for a given email.Tests: 22 test files added or updated across mobile, web, and app-shared, covering the presentation matrix, ledger admit/replay, the moderation router, the Terms gate, and the mobile purchase-screen states. Generated: 2 files updated (
0219_snapshot.json,_journal.json).Verification
Visual Changes
Kilo Pass purchase screen (iOS)
Android storefront cards
Unavailable:
Web-management:
Report AI response
Retryable toast:
Submitted with receipt:
PR discussion report / mute / block
Report receipt:
Muted author:
Blocked author:
UGC Terms gate
Cancel:
Accept:
Reviewer Notes