* [web] Remove SubscriptionFormatters, format subscription dates in the frontend
Replace the server-formatted nextPaymentDueAt/nextPaymentDueDate/
trialEndsAtFormatted fields with the raw periodEnd date and format in
the React dashboard (same strings as before, English + UTC). Also
consolidate hand-rolled trial checks onto isInFreeTrial /
SubscriptionHelper.isInTrial.
* [web] Remove the now-unused dateformat dependency
* [web] Address review: payment-dates unit test, UTC dates in acceptance test
Add a dedicated unit test asserting the exact formatted strings from
formatPaymentDate/formatPaymentDateTime, and use explicit UTC timestamps
in the SubscriptionDashboard acceptance fixtures instead of
local-timezone Date constructors.
GitOrigin-RevId: 16ac4f1098b14ef9f1478d22cf39c1357ead7dea
* [web] Add cancel-loss-messaging split test
Populate the cancel confirmation step with plan-specific losses when the
cancel-loss-messaging split test is enabled: "Are you sure you want to
cancel?" heading, the subscription end date, and a card listing what the
user loses moving to the Free plan (compile timeout, AI, collaborators,
history), with the lost benefit struck through next to its Free-plan
replacement.
Shown only to non-trial Student/Standard/Pro individual subscribers with
no other retention offer (trial extension or downgrade upsell) — those
keep the existing confirmation.
Copy matches the mockups in the ticket. No new analytics events; the
existing subscription-page-cancel-button-click / subscription-cancelled
funnel is used.
Part of #35765
* [web] Consolidate ActiveSubscription stories, add cancel confirmation story
Replace the 13 fixture-per-story exports with a single Dashboard story
driven by controls (subscription fixture select, flexible licensing,
cancel-loss-messaging variant), using the CSF factories pattern. Add a
CancelConfirmation story that renders the cancel confirmation step
directly so the cancel-loss-messaging variant can be previewed without
clicking through, with a Professional fixture available to see the Pro
loss rows.
* [web] Use withSplitTests for the ActiveSubscription stories
Register cancel-loss-messaging in defaultSplitTestsArgTypes and drive the
stories' variant from the shared withSplitTests decorator (radio control,
SplitTestContext provided from args) instead of hand-rolled meta tags and
SplitTestProvider. Generalize withSplitTests' typing so it composes with
CSF factories (preview.meta) as well as legacy meta objects.
* [web] Fix Storybook indexing of the ActiveSubscription stories
Storybook's static CSF indexer requires preview.meta() to receive an
object literal, so spread withSplitTests' argTypes/decorators into the
meta literal instead of wrapping the whole config in the helper call.
* [web] Compose ActiveSubscription stories from orthogonal controls
- Wrap the stories in #subscription-dashboard-root so the dashboard's
scoped styles (e.g. the cancel-loss strikethrough) apply in Storybook
- Replace the single fixture dropdown with independent controls (group,
plan, period, currency, trial, status, pending plan change, add-ons,
coupons, pending license change), building the subscription from the
fixtures' base + patches
- Guard impossible mixes with argTypes `if` conditions: individual-only
controls hide when group is on, and pendingLicenseChange only shows
for groups
* [web] Add student plan to the ActiveSubscription story controls
The cancel-loss-messaging variant has student-specific rows (AI
Assistant / 10 collaborators), so the plan control needs a student
option. Groups have no student plans, so for group subscriptions the
student choice maps to the Standard group fixture.
* [web] Provide ol-plans in the ActiveSubscription stories
The cancel confirmation's downgrade branch (monthly Standard is
isEligibleForDowngradeUpsell) looks up paid-personal in ol-plans and
spins forever when the meta tag is missing. Provide ol-plans and
ol-groupPlans from the fixtures, matching the test render helper.
GitOrigin-RevId: e8491c8d39ed059e42ea3442e8d70c3ef304b009
* update zod safePath validation to match existing behaviour in web
* test SafePath from web against zod validation
GitOrigin-RevId: a159f5078b5bf75eac4365c97e34e21e716290eb
* [web] gracefully handle failed oauth token exchange for reference managers
Mendeley/ReadCube redirect back to /:ref_provider/oauth/token-exchange with
`error` and `error_description` instead of `code` when a user declines consent.
completeAuth forwarded the whole query string on to third-party-references,
whose strict zod schema (d8c1679bb08) accepts only oauth_token, oauth_verifier
and code, so the exchange failed and the user got a 500.
The same forwarding also sent `state` on every callback, success or failure.
`state` is web's own CSRF token and is never read by third-party-references, so
it tripped the log-only schema on every single token exchange, and would have
broken linking outright under REQ_VALIDATION_MODE=enforce.
Forward only the credentials the exchange needs, and show a translated
notification in the Reference managers section rather than erroring. Validate
ref_provider against the known providers so the provider name can be
interpolated into the message without reflecting a URL segment.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* [web] fix providerName argument and validate before deriving refProvider
Address Copilot review on #36377:
- The token-exchange failure handler passed `false` positionally into the
`providerName` slot of _redirectWithLinkingError, so a failed exchange would
have rendered "your false account". Pass providerName and an explicit false.
- _getRefProviderBackendKey(req) ran before parseReq, so refProvider was derived
from raw, unvalidated req.params. Parse first and derive from the validated
params via _toRefProviderBackendKey; _getRefProviderBackendKey now delegates
to it so the other handlers are unchanged.
The completeAuth tests kept their assertions inside ctx.res.callback without
awaiting the controller, so the callback fired after the test had already
returned and the assertions were never enforced — which is why the suite was
green with the providerName bug in it. Await the controller and assert inline.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* [web] add logOnly to the completeAuth schema and guard the provider lookup
logOnly returns the raw input on a schema failure, so ref_provider can be an
arbitrary URL segment. Check it against the display-name table rather than
trusting the schema: unknown providers 404 in both validation modes, and
providerName is guaranteed non-null for the rest of the handler.
Also covers the schema half under REQ_VALIDATION_MODE=enforce, so dropping
ref_provider from completeAuthSchema fails a test rather than passing unnoticed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
GitOrigin-RevId: b98eb5414fe8b672f44aab8c3690f7637d434cd2
* Record a metric for Stripe API request statuses to monitor rate limiting
* Add on() stub to Stripe mock in StripeClient unit tests
* Record the Stripe rate-limit reason for 429 responses
* Add acceptance test for Stripe rate-limit reason metric
GitOrigin-RevId: 1674b389f5a7fee403f980b6e813a3e2df81063c
* Fix negative AI quota countdown after reset period elapses
Backend (`FeatureUsageRateLimiter`, `TokenUsageRateLimiter`): once the
24h period had lapsed, `remainingUsage`/`remainingTokens` reset to the
full allowance but `resetDate` kept pointing at the old `periodStart +
24h` (i.e. in the past). Advance to `now + 24h` in that case.
Frontend (`NewPlanPaywall`, `ErrorGetPremium`): the "wait X hours and Y
minutes" copy computed `restoreTime - now` without clamping, so a past
`restoreTime` produced negative hours/minutes (users saw "wait -118
hours and -35 minutes"). Clamp to 0.
* Address PR review on quota reset fix
- Keep `resetDate` tied to the stored `periodStart + PERIOD` rather
than jumping to `now + PERIOD` when the period has lapsed. The
moving-target variant unnecessarily reinterpreted a stable field
(existing consumers such as `use-ai-access-gate` check whether the
reset date is in the past).
- Clamp `remainingUsage` / `remainingTokens` at 0 so a concurrent
over-usage never leaks a negative count.
- Align the boundary check with `_checkRateLimit` / `checkUsage`
(inclusive `<=`).
- Cover the period boundary and the clamp in unit tests for both
`FeatureUsageRateLimiter` and `WorkbenchRateLimiter`.
* Truncate test fixtures to whole seconds
`resetDate` is serialized with `Date.toString()`, which has second
resolution, so fixtures with a millisecond component failed the
boundary assertions.
* Revert code changes; keep tests that document the bug
The fixes masked the symptom (negative countdown) without addressing
the underlying issue, and it is not fully understood yet. Revert the
rate limiters and the writefull paywall components to their original
behaviour, and instead pin the current behaviour in tests:
- `it.fails` tests document that `remainingUsage`/`remainingTokens`
go negative when usage exceeds the allowance — these flip red when
the bug is fixed, at which point they should become plain `it`.
- The lapsed-period tests assert (with explanatory comments) that
`resetDate` keeps pointing at the end of the lapsed period, which is
the server-side ingredient of the negative-countdown support ticket.
* Reapply quota fixes
Restores the post-review state of the rate limiters (clamp remaining
counts at 0, inclusive period boundary, resetDate tied to periodStart)
and the writefull paywall components (clamp countdown at 0).
Tests intentionally untouched in this commit; the two `it.fails` tests
now need flipping to plain `it` in a follow-up.
* Flip bug-documenting tests to plain assertions
The clamp fixes are reapplied, so the `it.fails` tests now assert the
fixed behaviour directly. Trim the explanatory comments.
* Set resetDate to `Date.now() + PERIOD_IN_MILLISECONDS` instead of the expired/stale date.
Co-authored-by: Mathias Jakobsen <mathias.jakobsen@overleaf.com>
* Update lapsed-period tests for the fresh resetDate
`resetDate` is now `Date.now() + PERIOD` once the period has lapsed, so
assert an approximately-fresh date instead of the stale period end.
---------
Co-authored-by: Mathias Jakobsen <mathias.jakobsen@overleaf.com>
GitOrigin-RevId: ad39627ab9e5ceccb2a61443b2b7d39c68600594
[web] Add Stripe wallet payments (Apple Pay / Google Pay) in analytics and remove conversion to "card"
GitOrigin-RevId: f6d404d35eb108d7ddaf8ca1b6c2164cb5100c9c