a tool for shared writing and social publishing

Unify the subscription flows behind src/subscriptions master

Email, atproto, and paid membership subscriptions each had their own notion of "subscribed" spread across the subscribe buttons, the manage panel, and the unsubscribe paths, so the three surfaces disagreed and every caller re-derived the state. Collapse them onto deriveSubscriptionState and a single fullUnsubscribe, replacing src/emailSubscription.ts with the src/subscriptions module. Muting email is now a subscriber row in the `unsubscribed` state rather than a new column. Cancelling a Stripe subscription is gated behind an allowStripeCancel flag that only the authenticated action sets. Unsubscribe links are followed by mail-client prefetches and Postmark's one-click List-Unsubscribe-Post, so the token route must never end a paid membership; it stops email and leaves billing intact. Whether a membership is live is decided by status alone, not by isActiveMembership, which reads a membership as inactive the moment current_period_end passes. Between period end and the renewal webhook, and throughout a past_due retry, the subscription is still live at Stripe and cancelling it needs the same confirmation an active one does. The email-notifications toggle only renders when something else keeps the subscription alive. For an email-only reader, turning it off is the unsubscribe, and it would drop `subscribed` and unmount the panel mid-interaction; they get the address as a plain row and the deliberate Unsubscribe button instead. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>


+1219 -831
28 changed files