Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Tags: hexclave/stack-auth

Tags

emulator-dev-20260516-d0202eee

Toggle emulator-dev-20260516-d0202eee's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
payments: rework refund flow to three-knob API (#1429)

## Summary
- Replaces per-entry refund schema with a flat `{ amount_usd,
revoke_product, end_subscription? }` shape; refund state is now derived
from bulldozer ledger rows (`refund:<sourceTxnId>:<uuid>`) instead of
the legacy `refundedAt` column, enabling multiple partial refunds up to
the remaining cap.
- Adds `invoice_id` for refunding any subscription invoice (start or
renewal), Stripe idempotency keys derived from `(tenancyId, sourceTxnId,
amount, prior_refunded)` so retries dedupe but intentional partials
don't collide, and a legacy backstop that rejects pre-rework
`refundedAt` purchases.
- Dashboard refund dialog rebuilt around the three toggles (revoke→end
coupling cascades into the UI); refund rows surface in the listing as
`type: "refund"` with `adjusted_by` linkage handling both new and legacy
formats.

## Implements
[STA2-52 — Build in refund logic for
payments](https://linear.app/stack-auth/issue/STA2-52/build-in-refund-logic-for-payments)

## Documented limitations (planned follow-up work)
These are called out in code comments and intentionally deferred to a
follow-up PR:
- **Cap-check race under concurrent refunds.** Bulldozer's embedded
`BEGIN/COMMIT` prevents an outer Prisma tx from scoping the writes, so
two concurrent refunds can both pass the cap check. Needs a
bulldozer-aware mutex or pending-refund-intent pattern. In practice
refunds are admin-only and rare, so the race window is small.
- **Stripe + DB non-atomicity on the DB-success → response-loss path.**
The Stripe idempotency key is keyed on `(tenancyId, sourceTxnId, amount,
priorRefunded)`, so a retry after Stripe-success → DB-fail self-heals
(Stripe dedupes; the next attempt writes the bulldozer row). The hole is
the reverse direction: if the bulldozer row commits but the response is
lost, a retry sees a higher `priorRefunded` and generates a fresh key —
Stripe would issue a second real refund. No out-of-band reconciliation
today.
- **Dashboard can't reach the `invoice_id` path.** Refund actions are
only enabled on `purchase` rows and the submit call never passes
`invoice_id`, so admins refunding a renewal must use the API directly.
Follow-up: enable the action on `subscription-renewal` rows and thread
`invoice_id` through.

## Architectural note
`active-subscription-end` and `item-quantity-expire` entries are **not**
emitted on the refund row itself. They're produced by the derived
sub-end transaction (`transactions.ts:158-228`) once Prisma
`subscription.endedAt` is updated, keeping the `expiresWhen` /
`when-repeated` semantics in one place. This is the main structural
divergence from the ticket's literal entry recipe.

## Review follow-ups addressed in this PR

**First-pass review:**
- **KnownError back-compat preserved**: `SubscriptionAlreadyRefunded` /
`OneTimePurchaseAlreadyRefunded` are once again thrown by the
legacy-`refundedAt` backstop, and `TestModePurchaseNonRefundable` is
thrown when an admin sends `amount_usd > 0` against a test-mode
purchase. Callers catching by error code keep working through the
rework.
- **Idempotency-key comment corrected**: now accurately describes the
`(tenancyId, sourceTxnId, amount, priorRefunded)` key and its
self-healing behaviour on the Stripe-success → DB-fail retry path (see
Documented limitations above for the remaining hole).
- **Renewal-invoice e2e coverage added**: new test sets up a live-mode
subscription via Stripe webhooks (`subscription_create` +
`subscription_cycle` invoices), refunds the renewal invoice via
`invoice_id`, and asserts the resulting `refund_transaction_id` starts
with `refund:sub-renewal:` and is linked back via `adjusted_by` on the
*renewal* row (not the start row). Plus negative cases:
cross-subscription `invoice_id` → 404, `invoice_id` on a one-time
purchase → SchemaError.

**Second-pass review:**
- **Idempotent sub-cancel error-code string fix**: the Stripe code for
re-cancelling an already-canceled sub is
`subscription_already_canceled`, not `subscription_canceled` — the
previous catch would have re-thrown.
- **End-only sub refund replay rejected**: when `amount=0, revoke=false,
end=true` and the sub is already `cancelAtPeriodEnd` or `endedAt`, throw
SchemaError. Otherwise `readPriorRefundSummary` doesn't see end-only
events and the call would be a forever-no-op accumulating empty refund
rows.
- **`revoke_product=true` with renewal `invoice_id` rejected**: the
product grant lives on the sub-start txn, not on renewal txns — a
renewal-scoped revocation would write a back-reference to a non-existent
entry. Forces admin to revoke against the start invoice (or the default
no-`invoice_id` call).
- **Refund row `id` matches the linkage**: the listing route now returns
the full refund txnId as `id` for `type: "refund"` rows so it matches
`adjusted_by.transaction_id` — the dashboard can join source rows to
their refund rows.
- **+2 e2e tests** for the above (end-only replay rejection,
revoke+renewal rejection).

**Third-pass review:**
- **Dashboard refund dialog seeds state on open**: previously the reset
block lived in `ActionDialog`'s `onOpenChange`, which doesn't fire on
the open transition for a controlled dialog. As a result the dialog
opened with the initial `useState` defaults (`amountUsd = '0'`), and an
admin submitting unchanged on a paid purchase would revoke/end at $0
instead of refunding the charged amount. The seed now runs in the menu
`onClick` before `setIsDialogOpen(true)`.
- **`SUBSCRIPTION_START_PRODUCT_GRANT_ENTRY_INDEX` corrected from 1 →
0**: the constant is persisted as `adjustedEntryIndex` on
product-revocation entries and copied through verbatim by
`mapLedgerEntry`. That mapper drops the hidden
`active-subscription-start` entry, so the public-API layout puts the
product grant at index 0. The prior value of `1` pointed at the
money-transfer entry (or out of range on test-mode subs) through the
public listing.
- **`amountTotal` cap gated behind a USD pre-flight**:
`SubscriptionInvoice` doesn't persist invoice currency, and the previous
code took `invoice.amountTotal` as USD cents directly. Now
`getTotalUsdStripeUnits` (which throws on non-USD pricing) is always
called first; `amountTotal` is only preferred as the actual cap after
that pre-flight succeeds.

## Test plan
- [x] `pnpm typecheck` — 28/28 pass
- [x] `pnpm lint` — 28/28 pass
- [x] `pnpm test run
apps/e2e/tests/backend/endpoints/api/v1/internal/transactions-refund.test.ts`
— **19/19 pass** (was 14/14 on the original PR; +3 for `invoice_id`
path: renewal refund happy path, unrelated `invoice_id` rejection,
`invoice_id` on OTP rejection; +2 for second-pass: end-only replay
rejection, revoke+renewal rejection)
- [x] curl smoke against
`/api/latest/internal/payments/transactions/refund` — unknown purchase →
404, no-op → 400, negative → 400, sub-revoke-without-end → 400
- [x] **Dashboard UI end-to-end re-run pending** — the original
agent-browser pass ran before the third-pass dialog-seed fix, so any
"money + revoke" submissions may have actually sent `amount_usd = "0"`.
Re-test before un-drafting: open the refund dialog from the menu,
confirm the amount field pre-fills with the charged amount, exercise
validation (negative / exceeds-cap / no-op), and submit both an
end-subscription-only sub refund and a money+revoke OTP refund; verify
bulldozer rows and Prisma `cancelAtPeriodEnd` updates.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Ledger-driven refund flow with stable refund IDs, invoice-aware
refunds, OTP/product-revocation support, tri-state end_action (now /
at-period-end / none), and API responses that include
refund_transaction_id.

* **Bug Fixes / Improvements**
* Deterministic Stripe idempotency, stronger replay protection,
refundable-amount caps, test-mode constraints, and transactions listing
updated to surface refunds.

* **Tests**
* Expanded unit and E2E coverage for new request shape, invoice paths,
money-unit conversion, and edge cases.

<!-- review_stack_entry_start -->

[![Review Change
Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/hexclave/stack-auth/pull/1429)

<!-- review_stack_entry_end -->
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

emulator-dev-20260516-b526e3b3

Toggle emulator-dev-20260516-b526e3b3's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
payments: rework refund flow to three-knob API (#1429)

## Summary
- Replaces per-entry refund schema with a flat `{ amount_usd,
revoke_product, end_subscription? }` shape; refund state is now derived
from bulldozer ledger rows (`refund:<sourceTxnId>:<uuid>`) instead of
the legacy `refundedAt` column, enabling multiple partial refunds up to
the remaining cap.
- Adds `invoice_id` for refunding any subscription invoice (start or
renewal), Stripe idempotency keys derived from `(tenancyId, sourceTxnId,
amount, prior_refunded)` so retries dedupe but intentional partials
don't collide, and a legacy backstop that rejects pre-rework
`refundedAt` purchases.
- Dashboard refund dialog rebuilt around the three toggles (revoke→end
coupling cascades into the UI); refund rows surface in the listing as
`type: "refund"` with `adjusted_by` linkage handling both new and legacy
formats.

## Implements
[STA2-52 — Build in refund logic for
payments](https://linear.app/stack-auth/issue/STA2-52/build-in-refund-logic-for-payments)

## Documented limitations (planned follow-up work)
These are called out in code comments and intentionally deferred to a
follow-up PR:
- **Cap-check race under concurrent refunds.** Bulldozer's embedded
`BEGIN/COMMIT` prevents an outer Prisma tx from scoping the writes, so
two concurrent refunds can both pass the cap check. Needs a
bulldozer-aware mutex or pending-refund-intent pattern. In practice
refunds are admin-only and rare, so the race window is small.
- **Stripe + DB non-atomicity on the DB-success → response-loss path.**
The Stripe idempotency key is keyed on `(tenancyId, sourceTxnId, amount,
priorRefunded)`, so a retry after Stripe-success → DB-fail self-heals
(Stripe dedupes; the next attempt writes the bulldozer row). The hole is
the reverse direction: if the bulldozer row commits but the response is
lost, a retry sees a higher `priorRefunded` and generates a fresh key —
Stripe would issue a second real refund. No out-of-band reconciliation
today.
- **Dashboard can't reach the `invoice_id` path.** Refund actions are
only enabled on `purchase` rows and the submit call never passes
`invoice_id`, so admins refunding a renewal must use the API directly.
Follow-up: enable the action on `subscription-renewal` rows and thread
`invoice_id` through.

## Architectural note
`active-subscription-end` and `item-quantity-expire` entries are **not**
emitted on the refund row itself. They're produced by the derived
sub-end transaction (`transactions.ts:158-228`) once Prisma
`subscription.endedAt` is updated, keeping the `expiresWhen` /
`when-repeated` semantics in one place. This is the main structural
divergence from the ticket's literal entry recipe.

## Review follow-ups addressed in this PR

**First-pass review:**
- **KnownError back-compat preserved**: `SubscriptionAlreadyRefunded` /
`OneTimePurchaseAlreadyRefunded` are once again thrown by the
legacy-`refundedAt` backstop, and `TestModePurchaseNonRefundable` is
thrown when an admin sends `amount_usd > 0` against a test-mode
purchase. Callers catching by error code keep working through the
rework.
- **Idempotency-key comment corrected**: now accurately describes the
`(tenancyId, sourceTxnId, amount, priorRefunded)` key and its
self-healing behaviour on the Stripe-success → DB-fail retry path (see
Documented limitations above for the remaining hole).
- **Renewal-invoice e2e coverage added**: new test sets up a live-mode
subscription via Stripe webhooks (`subscription_create` +
`subscription_cycle` invoices), refunds the renewal invoice via
`invoice_id`, and asserts the resulting `refund_transaction_id` starts
with `refund:sub-renewal:` and is linked back via `adjusted_by` on the
*renewal* row (not the start row). Plus negative cases:
cross-subscription `invoice_id` → 404, `invoice_id` on a one-time
purchase → SchemaError.

**Second-pass review:**
- **Idempotent sub-cancel error-code string fix**: the Stripe code for
re-cancelling an already-canceled sub is
`subscription_already_canceled`, not `subscription_canceled` — the
previous catch would have re-thrown.
- **End-only sub refund replay rejected**: when `amount=0, revoke=false,
end=true` and the sub is already `cancelAtPeriodEnd` or `endedAt`, throw
SchemaError. Otherwise `readPriorRefundSummary` doesn't see end-only
events and the call would be a forever-no-op accumulating empty refund
rows.
- **`revoke_product=true` with renewal `invoice_id` rejected**: the
product grant lives on the sub-start txn, not on renewal txns — a
renewal-scoped revocation would write a back-reference to a non-existent
entry. Forces admin to revoke against the start invoice (or the default
no-`invoice_id` call).
- **Refund row `id` matches the linkage**: the listing route now returns
the full refund txnId as `id` for `type: "refund"` rows so it matches
`adjusted_by.transaction_id` — the dashboard can join source rows to
their refund rows.
- **+2 e2e tests** for the above (end-only replay rejection,
revoke+renewal rejection).

**Third-pass review:**
- **Dashboard refund dialog seeds state on open**: previously the reset
block lived in `ActionDialog`'s `onOpenChange`, which doesn't fire on
the open transition for a controlled dialog. As a result the dialog
opened with the initial `useState` defaults (`amountUsd = '0'`), and an
admin submitting unchanged on a paid purchase would revoke/end at $0
instead of refunding the charged amount. The seed now runs in the menu
`onClick` before `setIsDialogOpen(true)`.
- **`SUBSCRIPTION_START_PRODUCT_GRANT_ENTRY_INDEX` corrected from 1 →
0**: the constant is persisted as `adjustedEntryIndex` on
product-revocation entries and copied through verbatim by
`mapLedgerEntry`. That mapper drops the hidden
`active-subscription-start` entry, so the public-API layout puts the
product grant at index 0. The prior value of `1` pointed at the
money-transfer entry (or out of range on test-mode subs) through the
public listing.
- **`amountTotal` cap gated behind a USD pre-flight**:
`SubscriptionInvoice` doesn't persist invoice currency, and the previous
code took `invoice.amountTotal` as USD cents directly. Now
`getTotalUsdStripeUnits` (which throws on non-USD pricing) is always
called first; `amountTotal` is only preferred as the actual cap after
that pre-flight succeeds.

## Test plan
- [x] `pnpm typecheck` — 28/28 pass
- [x] `pnpm lint` — 28/28 pass
- [x] `pnpm test run
apps/e2e/tests/backend/endpoints/api/v1/internal/transactions-refund.test.ts`
— **19/19 pass** (was 14/14 on the original PR; +3 for `invoice_id`
path: renewal refund happy path, unrelated `invoice_id` rejection,
`invoice_id` on OTP rejection; +2 for second-pass: end-only replay
rejection, revoke+renewal rejection)
- [x] curl smoke against
`/api/latest/internal/payments/transactions/refund` — unknown purchase →
404, no-op → 400, negative → 400, sub-revoke-without-end → 400
- [x] **Dashboard UI end-to-end re-run pending** — the original
agent-browser pass ran before the third-pass dialog-seed fix, so any
"money + revoke" submissions may have actually sent `amount_usd = "0"`.
Re-test before un-drafting: open the refund dialog from the menu,
confirm the amount field pre-fills with the charged amount, exercise
validation (negative / exceeds-cap / no-op), and submit both an
end-subscription-only sub refund and a money+revoke OTP refund; verify
bulldozer rows and Prisma `cancelAtPeriodEnd` updates.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Ledger-driven refund flow with stable refund IDs, invoice-aware
refunds, OTP/product-revocation support, tri-state end_action (now /
at-period-end / none), and API responses that include
refund_transaction_id.

* **Bug Fixes / Improvements**
* Deterministic Stripe idempotency, stronger replay protection,
refundable-amount caps, test-mode constraints, and transactions listing
updated to surface refunds.

* **Tests**
* Expanded unit and E2E coverage for new request shape, invoice paths,
money-unit conversion, and edge cases.

<!-- review_stack_entry_start -->

[![Review Change
Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/hexclave/stack-auth/pull/1429)

<!-- review_stack_entry_end -->
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

emulator-dev-20260516-049c557a

Toggle emulator-dev-20260516-049c557a's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
Project transfer page redesign (#1309)

<!--

Make sure you've read the CONTRIBUTING.md guidelines:
https://github.com/stack-auth/stack-auth/blob/dev/CONTRIBUTING.md

-->

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Reusable transfer confirmation UI with clear loading, success, and
error states.
* Neon-specific transfer flow added, guiding sign-in, account switching,
or accepting transfers.
* Custom integration transfer flow with streamlined confirm/check
behavior.
* Improved transfer sign-up redirect so users return to the correct page
after auth.

* **Bug Fixes**
  * Consistent messaging for missing/invalid/expired transfer codes.
  * Safer widget “Reload” handling when reset may be unavailable.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---

## Summary

Redesigns the **custom integration** project-transfer confirmation page
(`/integrations/custom/projects/transfer/confirm`) onto the new
design-components system (`DesignCard` + `DesignAlert` + `DesignButton`
+ `DesignInput`). The presentational shell is extracted into a reusable
`ProjectTransferConfirmView` so the route file only handles state + API
calls. The legacy Neon transfer page is split out unchanged into its own
client component to keep the existing Neon × Stack co-branded UI intact.

---

## Screenshots — before and after

> Captured against `http://localhost:8101` at 1280×900. Dev-only
overlays (outdated-version banner, console toast, DEV badge) are hidden
via injected CSS for clarity.

### Custom integration — missing transfer code

Visiting `/integrations/custom/projects/transfer/confirm` with no
`?code=…` query param.

| Before (`dev`) | After (this PR) |
| --- | --- |
|
![](https://gist.githubusercontent.com/aadesh18/85342c8cb1890b11e7b44c9baff34283/raw/before-custom-missing__light.png)
|
![](https://gist.githubusercontent.com/aadesh18/85342c8cb1890b11e7b44c9baff34283/raw/after-custom-missing__light.png)
|
|
![](https://gist.githubusercontent.com/aadesh18/85342c8cb1890b11e7b44c9baff34283/raw/before-custom-missing__dark.png)
|
![](https://gist.githubusercontent.com/aadesh18/85342c8cb1890b11e7b44c9baff34283/raw/after-custom-missing__dark.png)
|

Before was a raw `"Error: No transfer code provided."` line. After is a
dedicated `DesignAlert` with an explanation and recovery instructions.

### Custom integration — invalid / expired code (check endpoint fails)

| Before (`dev`) | After (this PR) |
| --- | --- |
|
![](https://gist.githubusercontent.com/aadesh18/85342c8cb1890b11e7b44c9baff34283/raw/before-custom-error__light.png)
|
![](https://gist.githubusercontent.com/aadesh18/85342c8cb1890b11e7b44c9baff34283/raw/after-custom-error__light.png)
|
|
![](https://gist.githubusercontent.com/aadesh18/85342c8cb1890b11e7b44c9baff34283/raw/before-custom-error__dark.png)
|
![](https://gist.githubusercontent.com/aadesh18/85342c8cb1890b11e7b44c9baff34283/raw/after-custom-error__dark.png)
|

Before showed the raw backend error string (`Request validation failed
on POST …`). After uses a `DesignCard` with the `ArrowsLeftRightIcon`, a
friendlier "This transfer can't continue" copy in an inline
`DesignAlert`, the Stack Auth logomark in the actions slot, and an
explicit **Close** button to dismiss.

### Neon integration — legacy UI preserved

The Neon page (`/integrations/neon/projects/transfer/confirm`) was
deliberately **not** redesigned — it still uses the Neon × Stack
co-branded card so partner-facing copy/branding stay identical. It's now
its own client component (`neon-transfer-confirm-page.tsx`) instead of
sharing the redesigned one.

| Before (`dev`) | After (this PR) |
| --- | --- |
|
![](https://gist.githubusercontent.com/aadesh18/85342c8cb1890b11e7b44c9baff34283/raw/before-neon-error__light.png)
|
![](https://gist.githubusercontent.com/aadesh18/85342c8cb1890b11e7b44c9baff34283/raw/after-neon-error__light.png)
|
|
![](https://gist.githubusercontent.com/aadesh18/85342c8cb1890b11e7b44c9baff34283/raw/before-neon-error__dark.png)
|
![](https://gist.githubusercontent.com/aadesh18/85342c8cb1890b11e7b44c9baff34283/raw/after-neon-error__dark.png)
|

Same shell on both sides — copy was tightened slightly ("Return to your
Neon dashboard and start the transfer again") and the raw API error
string is gone.

---

## What changed

- **New**
`apps/dashboard/src/components/project-transfer-confirm-view.tsx` —
purely presentational `ProjectTransferConfirmView`. Owns the
design-components shell, the loading spinner, the signed-in vs
signed-out branches of the success state (with `DesignInput` + "Use a
different account" button), and the error / missing-code alerts.
- **New**
`apps/dashboard/src/app/(main)/integrations/neon-transfer-confirm-page.tsx`
— extraction of the legacy Neon UI (Neon logo, Stack logo, "Project
transfer" header, Card / CardContent / CardFooter). Behaviour and copy
match the previous `transfer-confirm-page` exactly when `type ===
"neon"`.
- **Rewritten**
`apps/dashboard/src/app/(main)/integrations/transfer-confirm-page.tsx` —
now hard-coded to the `custom` integration (no more `type` prop), defers
UI to `ProjectTransferConfirmView`, and exports a
`TransferConfirmMissingCodeView` used by the route when `code` is absent
from the URL.
- **Route plumbing**
- `app/(main)/integrations/custom/projects/transfer/confirm/page.tsx` —
renders the redesigned flow, falls back to
`TransferConfirmMissingCodeView` when `code` is missing.
- `app/(main)/integrations/neon/projects/transfer/confirm/page.tsx` —
points at the new dedicated Neon client component.
- **New** `apps/dashboard/src/lib/stack-app-internals.ts` — consolidates
the symbol-keyed `getStackAppInternals(app)` helper (and
`stackAppInternalsSymbol`) into one module with a JSDoc explainer +
runtime type guard, replacing scattered `as any` casts.
- **New** `apps/dashboard/src/lib/transfer-utils.ts` —
`buildTransferSignUpUrl()` helper so the route file + the view stay in
sync on the `/handler/signup?after_auth_return_to=…` query construction.

---

## Bot review follow-ups addressed in this PR

- **Fail-loud assertions for unset handlers** in the success state of
`ProjectTransferConfirmView` (`StackAssertionError` instead of silent
no-op).
- **SSR safety:** moved every `window.location` read into client-only
handlers / `useEffect`s — the page was previously evaluating it at
module load.
- **Friendly error fallback** when the backend `/check` endpoint throws
— replaces the raw `KnownError<…>` message with "This transfer link is
invalid, has expired, or has already been used. Open the original link
from the partner or integrations dashboard, or start the transfer
again."
- **`runAsynchronouslyWithAlert`** around every async `onClick`
(Transfer, Sign in, Switch account, Close) so unhandled rejections
surface to the user.
- **JSX entity bug fix:** `&apos;` was a string-attribute literal, not a
JSX expression — converted to a JSX expression so it renders as `'`.
- **`window.close()` removal** in error state — replaced with a Close
button that resets local state, so users on a fresh tab (no opener)
aren't stuck.
- **`getStackAppInternals` consolidated** — previously three independent
copies (here + two in `projects/page-client.tsx`). Now one helper with a
runtime type guard instead of `as any`, plus a comment explaining the
symbol-keyed SDK escape hatch.
- **Widget-playground reset:** the original change here turned out to
duplicate a deliberate prior fix on `dev` (N2D4, `e68015909d "Fix
lint"`). Reverted in `fe92689eb` so we don't fight that fix.

---

## Notes for reviewers

- **Start with** `components/project-transfer-confirm-view.tsx`.
Everything reviewer-interesting is in the props shape
(`ProjectTransferConfirmUiState` union, `onPrimary` / `onCancel` /
`onSwitchAccount` callbacks). The route file just wires those to the
`getStackAppInternals(app).sendRequest(...)` calls.
- **The Neon page was intentionally not migrated.** Partner-facing
co-branding (Neon logo × Stack logo, "Neon would like to transfer…"
copy) is unchanged — flag it if you think it should be brought onto
design-components too, but the goal of this PR was only the custom flow.
- **API surface is unchanged** — same
`/integrations/custom/projects/transfer/confirm/check` and
`/integrations/custom/projects/transfer/confirm` endpoints, same request
bodies, same redirect to `/projects/{project_id}` on success.
- **Success state isn't in the screenshots** because reproducing it
locally needs a real transfer code (the `/check` endpoint validates the
code against the DB). It uses the same `DesignCard` shell with either a
`DesignInput` showing the receiving account + a "Use a different
account" outline button (signed-in branch), or a `DesignAlert
variant="info"` prompting sign-in (signed-out branch). Worth manually
testing on a real transfer before merging.

## Test plan

- [ ] Visit `/integrations/custom/projects/transfer/confirm` with no
`code` → renders the "transfer link is incomplete" alert (screenshots
above)
- [ ] Visit
`/integrations/custom/projects/transfer/confirm?code=invalid` → renders
the redesigned card with the friendly error inside a `DesignAlert
variant="error"` and a working Close button
- [ ] Trigger a real custom-integration transfer end to end → loading
spinner, success state, "Accept transfer" works while signed in, "Sign
in" deep-links to `/handler/signup?after_auth_return_to=…` while signed
out
- [ ] Visit `/integrations/neon/projects/transfer/confirm?code=…` →
unchanged legacy Neon × Stack co-branded card
- [ ] Light + dark mode visual sanity (screenshots above are the
canonical reference)

---------

Co-authored-by: Aadesh Kheria <[email protected]>
Co-authored-by: aadesh18 <[email protected]>

emulator-dev-20260515-a9623d97

Toggle emulator-dev-20260515-a9623d97's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
[Refactor] [Fix] Remove default prod creation (#1350)

With the new bulldozer rework we dont support default products anymore.
Users are encouraged to currently manually handle granting products to
their end users.

We block api requests and new product creations that attempt to set no
price, and we remove any options to set include-by-default. We also
migrate users' existing product snapshots in `Subscriptions`,
`OneTimePurchases`, and `ProductVersions` to have no price set if it's
an include-by-default product. This will make it so that next time a
user goes onto their products page, they will be informed that the
pricing is invalid and it is no longer delivered by default.

Note, however, that these products will still be providing items and the
like to the users who have them.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Bug Fixes**
* Migrated legacy product snapshots so missing included-items no longer
break readers.
* Removed deprecated "include-by-default" pricing sentinel; pricing now
requires explicit price entries and write validation rejects the old
sentinel.

* **Chores**
* Simplified dashboard pricing flows: create/edit/save now use explicit
prices and surface an alert when a formerly implicit free plan needs an
explicit $0 price.
* Config overrides and stored data are auto-normalized to explicit price
objects.

* **Tests**
* Updated and added tests covering migration, validation, and switching
behavior for explicit prices.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: mantrakp04 <[email protected]>
Co-authored-by: Mantra <[email protected]>

emulator-main-20260513-3385d6e2

Toggle emulator-main-20260513-3385d6e2's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
[Fix] recover stale external db requests (#1428)

Failures between claiming and the deletion of outgoing requests from the
handler can leave requests stale and never clean them up. Some of these
requests may also have duplicates that are fresh in the outgoing queue.
These requests need to be deleted or retried.

It's important to still log the stale requests to sentry so the root
cause can be investigated.


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Bug Fixes**
* Improved detection and recovery of stale outgoing requests; telemetry
now records precise reset/deleted counts and includes sampled affected
IDs.
* Added an early fast path to skip unnecessary external calls when there
are no pending requests.

* **Refactor**
* Consolidated stale-request handling into a dedicated helper and
optimized recovery logic; poller telemetry now includes claim-limit
attributes.

[![Review Change
Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/hexclave/stack-auth/pull/1428)
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

emulator-dev-20260513-3385d6e2

Toggle emulator-dev-20260513-3385d6e2's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
[Fix] recover stale external db requests (#1428)

Failures between claiming and the deletion of outgoing requests from the
handler can leave requests stale and never clean them up. Some of these
requests may also have duplicates that are fresh in the outgoing queue.
These requests need to be deleted or retried.

It's important to still log the stale requests to sentry so the root
cause can be investigated.


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Bug Fixes**
* Improved detection and recovery of stale outgoing requests; telemetry
now records precise reset/deleted counts and includes sampled affected
IDs.
* Added an early fast path to skip unnecessary external calls when there
are no pending requests.

* **Refactor**
* Consolidated stale-request handling into a dedicated helper and
optimized recovery logic; poller telemetry now includes claim-limit
attributes.

[![Review Change
Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/hexclave/stack-auth/pull/1428)
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

emulator-dev-20260513-4648fc18

Toggle emulator-dev-20260513-4648fc18's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
[Feat] new scripts on migrate/seed/init run for internal (#1421)

### Context

One script grants free plan to any team which is a customer of the
internal project who doesnt have it already.
We also want to migrate our users (internal) to the latest version of
their products.

Needed because some subs on dev right now dont have a plan. And internal
isnt using latest version of its own growth plan.

### Describing the Paths we want to Account for

1. Users on production who currently don't have a plan should get free
plans, since this script is run with every migrate
2. Users on production should get the latest version of each plan of
ours. So a forced migration to latest version of internal project plans
3. No other project's products/product lines should be affected. They
will continue to have product versioning
4. 2 should apply to test mode subscriptions as well, on top of stripe
subscriptions. All of them should be refreshed
5. Internal project itself should get latest version of its own growth
plan
6. If the bulldozer write fails, we should be able to recover on next
migration (this should already be handled by init bulldozer script,
because it checks if prisma db and bulldozer db are out of sync)
7. if the regenerate or backfill fail, we should be able to recover just
by rerunning the script
8. Product version table should not balloon. No table should really
balloon



### What I've tested on local
1. Put in 1000 db subscription rows, made them all stale and then ran
the regen script. It took about 6 minutes to update all of them, and it
was idempotent so rerunning it again did nothing.
2. With proper stripe keys I switched off of test mode on the internal
app, granted a product to a new team and updated the product's item
list. At this point I checked and the new team had the outdated version
of the product. Then I ran the regen script and the new team was moved
to latest product version.
3. Tried the above with the internal team's growth plan too and it
worked as well.
4. Backfill actually grants free plan


### Deployment strategy in prod
Run the backfill and the regen scripts once each after your migrations
on the prod db.
`pnpm db:backfill-internal-free-plans` will make sure every team has a
free plan at least if they dont have an existing plan (and it is
idempotent).
After that, run `pnpm db:regen-internal-subscriptions-to-latest` which
will migrate every user to the latest version of their plan (i.e latest
snapshot). This should also be idempotent.


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Automated backfill to grant internal free plans to qualifying billing
teams.
* Regeneration tool to refresh internal subscription snapshots to the
latest product versions.

* **Chores**
* Added CLI commands and package scripts to run backfill and regen jobs.
  * Database init now runs payment initialization before backfill/regen.

* **Tests**
* Integration and unit tests added/updated to validate backfill,
regeneration, and free-plan idempotency.

[![Review Change
Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/hexclave/stack-auth/pull/1421)
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

emulator-dev-20260512-e5035871

Toggle emulator-dev-20260512-e5035871's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
Fix null-unsafe payments config validation for partial overrides (#1363)

## Summary
- Make the `branchPaymentsSchema` custom validator tolerant of partial
override objects
- Avoid crashing when `payments.products` or `payments.productLines` are
absent during validation
- Add regression tests for partial configs plus the existing
missing-line and customer-type mismatch cases

## Testing
- Added Vitest coverage for partial payments configs and validation
failures
- Lint passed for the touched schema files
- Typecheck passed for `packages/stack-shared`

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Bug Fixes**
* Improved validation robustness with stricter type-safety checks for
payment-related data configurations.
  * Enhanced error messages for clearer feedback on validation failures.

* **Tests**
* Added comprehensive test coverage for edge cases including missing
configurations and type mismatches.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

emulator-dev-20260512-e880df12

Toggle emulator-dev-20260512-e880df12's commit message
Add functionality to restrict or unrestrict users

emulator-dev-20260512-e95ccdea

Toggle emulator-dev-20260512-e95ccdea's commit message
Remove unused GitHub Action