Skip to content

Production Release: Peanut Card UI + Decomplexify + Native app + Manteca collateral#1984

Merged
Hugo0 merged 542 commits into
mainfrom
dev
May 14, 2026
Merged

Production Release: Peanut Card UI + Decomplexify + Native app + Manteca collateral#1984
Hugo0 merged 542 commits into
mainfrom
dev

Conversation

@Hugo0

@Hugo0 Hugo0 commented May 12, 2026

Copy link
Copy Markdown
Contributor

Production Release — FE

Companion BE PR: peanutprotocol/peanut-api-ts#747
Release tracker: https://www.notion.so/peanutprotocol/Release-Process-21e83811757980b79393db870361fc5d
QA tracker: https://www.notion.so/peanutprotocol/QA-For-Prod-Release-card-decomplexify-35983811757980afa1b8ffccb4759dc0

⚠️ Merge order: BE migrations → BE PR → FE PR. Never reverse. Vercel must wait for assertAllPrismaTablesExist to pass on the backend.

Stats

  • 73 merged feature/fix PRs (~423 commits ahead of main)
  • Card flow end-to-end · Decomplexify follow-ups · Native app (Android) · Manteca collateral

Major themes

1. Peanut Card UI — new product, whitelist-gated

UI is hidden unless users.card_access_granted_at is non-null. Card screens, lifecycle, history, virtual art.

Key PRs: card-ui (#1904 merge) · #1909 activity history · #1918 KYC required · #1919/#1920 denied screen + followup · #1926 details TC checkbox · #1931 card improvements · #1944/#1945 pay icon + titlecase · #1946 activity card USD guard · #1960 feat/fix-card · #1958 access bypass maintenance · #1961 lock/cancel/collateral return · #1967 push notifications · #1979 decline rebalance polish · #1981 auth lifecycle

2. Decomplexify follow-ups

UI side of the ledger cutover. New entry kinds, type gates, conditional history hooks.

3. Native app (Android)

4. Manteca / spend bundle

5. KYC + Sumsub

6. Cross-chain & withdraw fixes

7. Passkey / auth fixes

8. DevX / cleanup

ENV vars (set on Vercel before deploy)

NEXT_PUBLIC_ZERO_DEV_BUNDLER_URL=
NEXT_PUBLIC_ZERO_DEV_PAYMASTER_URL=

⚠️ Known issues still in flight

To be fixed on dev before merge (Hugo, in a branched session):

  • Failed-to-load contact error on contacts view (staging)
  • Cancelled sendlink: verify receipt fields & CTAs after fix(receipt): keep sender data visible on cancelled sendlinks #1966
  • Tx receipt regression (decomplexify)
  • Decomplexify icon sizing follow-ups
  • Founder House badge hotfix is on main — touches public/badges/founder_house.png, src/components/Badges/badge.utils.ts, src/components/Invites/InvitesPage.tsx. Needs sync-merge into dev before this PR is mergeable.

Test plan

  • CI green: typecheck, prettier, npm test, npm run build
  • Full Nutcracker screenshots (mobile viewport)
  • Card flow: signup → request → KYC → unlock → spend → activity (Jota + Kushagra)
  • Sendlink: create / claim / cancel — verify single history item + receipt CTAs
  • Cross-chain withdraw across same-chain and bridge cases
  • Manteca QR pay + withdraw on staging
  • Native Android build smoke (Play signing intact)
  • Whitelisted user sees card UI; non-whitelisted does NOT

Release execution

  1. Wait for BE PR merged + boot green
  2. Vercel env vars set
  3. Merge this PR
  4. Verify Vercel production URL responds
  5. Maintenance banner down
  6. Post-release smoke: home / send / receive / history / card UI gating

jjramirezn and others added 30 commits April 30, 2026 13:45
chore: remove residual Squid naming + dead types after Rhino SDA cutover
The Mobula calls in src/app/actions/tokens.ts ran in the browser bundle
(no 'use server', client-side `unstable_cache` shim, imported by client
hooks). `process.env.MOBULA_API_*` is undefined client-side, so the URL
collapsed to "undefined/api/1/market/data?…" — relative URL caught by
the Next.js dev server (200 from a default page handler — Mobula was
never reached). Native (Capacitor) builds also can't use server actions,
so server-side env reads are not an option.

  - New `src/services/tokens-price.ts` calls peanut-api-ts via apiFetch:
      GET /tokens/price            (replaces fetchTokenPrice)
      GET /tokens/wallet-portfolio (replaces fetchWalletBalances)
  - `useTokenPrice`, `Claim.tsx`, and `recover-funds/page.tsx` swapped
    onto the new service.
  - `estimateTransactionCostUsd` (still in app/actions/tokens.ts) now
    imports fetchTokenPrice from the service for its native-token-price
    lookup.
  - Deletes the broken Mobula bodies + IMobula* types + MOBULA_API_*
    consts from app/actions/tokens.ts. fetchTokenDetails (on-chain RPC,
    no Mobula) stays.

`src/app/api/health/mobula/route.ts` is a Next.js Route Handler running
server-side, so it can keep direct Mobula access. Untouched.

Pairs with peanut-api-ts #689.
apiFetch with /api/peanut${path} only works for paths that have an
explicit route handler at src/app/api/peanut/<path>/route.ts. There's
no catch-all there, so /api/peanut/tokens/price was silently served
the layout HTML (200 with <!doctype html>); response.json() then threw,
queryFn caught and returned null, and selectedTokenData stayed undefined
— Review button disabled on ETH selection.

serverFetch auto-routes by method through the existing catch-alls under
/api/proxy/get/[...slug] (GET) and /api/proxy/[...slug] (POST), which
do forward to PEANUT_API_URL with the api-key header.

Also drops the temporary debug overlay below the Review button.
Capacitor builds have no Next.js server, so any /api/proxy/* or
/api/peanut/* path is dead. Calling PEANUT_API_URL directly via
fetchWithSentry works identically on web (CORS-allowed origin) and
native (direct HTTPS). The endpoints are public on the backend with
per-IP rate limit + CORS — no api-key needed.
Pairs with peanut-api-ts: backend now accepts any token Rhino supports.
inferTokenSymbol returns whatever the FE's curated peanutTokenDetails
lists (ETH, WETH, …) instead of nulling out non-stablecoins. The token
selector already shows the curated list — this just lets the calculate
step pass through without throwing 'Cannot infer Rhino token symbol'.
/charges goes through /api/proxy/withFormData (multipart) which was
silently dropping the JWT. Backends that gate on auth (or soft-auth
for owner resolution) saw no user — withdraw to external addresses
FK-violated on transaction_intents_user_id_fkey because the
authenticated creator was unreachable. Parity with the JSON proxy at
/api/proxy/[...slug].
Same fix as services/tokens-price.ts. /api/peanut/<path> has no route
handler — Next.js silently returns the layout HTML so response.json()
threw 'Unexpected token <'. Direct call to PEANUT_API_URL with the JWT
manually forwarded for the backend's verifyAuth preHandler. Works on
web (CORS) and native (no Next.js server).
useCrossChainTransfer now branches by destination token:

  - USDC/USDT  → SDA (existing 'transfer-and-forget' UX, webhook settles)
  - everything else → bridge quote/commit (user signs calldata to Rhino's
    bridge contract directly)

Bridge path produces the same {transactions, receiveAmount, feeUsd}
shape as SDA so the Confirm view doesn't need a code change. Adds:

  - quoteExpiresAt / isQuoteExpired — Rhino quotes live ~60s; consumers
    can re-calculate before commit if the user lingered
  - commitmentId — needed to poll bridge status after the user signs
  - pollBridgeStatus(id) — 3s-interval poll until COMPLETED/FAILED/EXPIRED

Pairs with peanut-api-ts /rhino/bridge/* routes (PR #689).
Pairs with peanut-api-ts: backend branches getUserQuote vs
getSwapUserQuote on tokenIn/tokenOut equality. FE always passes USDC
(Peanut wallet token) as tokenIn; tokenOut is whatever the user
selected. isSwap echoes back from quote into commit so the backend
picks the right finalisation path.
isCrossChainWithdrawal only compared chain IDs, so USDC → ETH on
Arbitrum (same chain, different token) silently took the same-chain
collateral-only path: sendMoney does USDC.transfer(recipient, amount),
recipient gets USDC, the user-selected ETH never happens.

Use the (isXChain || isDiffToken) booleans from useCrossChainTransfer —
the hook already computes them and routes calculate() to the bridge
swap path for non-stablecoin destinations. Now the confirm-side gate
matches: cross-chain OR cross-token both go through sendTransactions,
which runs the calldata the hook prepared.
…erOp

Rhino's bridge contract pulls USDC from the kernel SA via transferFrom,
which requires an allowance. Without an approve in the same batch, the
bridge call reverts during simulation with 'ERC20: transfer amount
exceeds allowance' (paymaster rejects accordingly). Same pattern Squid
used in the legacy flow.

Approve + bridge sent as a 2-call batch in one userOp so they're atomic;
the kernel encodes them through executeBatch.
Rhino rejects mode='receive' on cross-chain swap routes with
'InvalidRequest: Receive mode is not supported for the selected
tokens'. Same-chain swap accepts 'receive', so keep that for the UX
'merchant gets X' framing; cross-chain falls back to 'pay' where the
input amount is the source USDC the user spends and Rhino computes
the destination output.
Rhino has two distinct deposit primitives:
  - same-chain atomic swap → getSwapCalldata returns Rhino's swap-contract
    call data; user signs it directly
  - cross-chain (bridge or swap) → user calls
    bridgeContract.depositWithId(tokenAddress, amount, commitmentId)
    (the SDK's DVFDepositContract.handleTokenDeposit path)

Backend's commitBridgeQuote now branches on isSameChainSwap and returns
either {kind: 'swap-calldata', calldata, contractAddress} or
{kind: 'deposit-with-id', contractAddress}. FE constructs the
depositWithId call itself for cross-chain — no Rhino API for it.

Cross-chain commitmentId is the hex-encoded BigInt of the quoteId
(SDK convention).

Pairs with peanut-api-ts /rhino/bridge/commit changes.
fix(tokens): fetch token price + wallet balances via api-ts backend
…apply

The immediate post-Sumsub re-apply raced against Sumsub's auto-review:
backend would still see `incomplete`, the WebSDK would re-mount against an
already-approved applicant, and the user would land back on the "Start
Secure Verification" interstitial. Tapping the close button after re-entry
also showed a misleading "Stop verification?" warning because the
3s early-event guard suppressed the GREEN signal that would otherwise
flip the submitted flag.

- Poll applyForCard 1s/15s while showing the pending screen, then advance
  on the first non-incomplete response (timeout surfaces a retry message).
- Treat early `completed + GREEN` events as evidence the user already
  submitted (suppresses the close-warning) without auto-closing — the
  guard's original purpose (not closing on stale RED+RETRY) stays intact.

Logic extracted to two pure helpers with unit coverage.
`handleApply` and `handleSumsubComplete` had near-identical
main-kyc-required / terms-required / default branching. Extract to
`advanceFromApplyResponse` so the two paths can't drift, and shrink
`card/page.tsx` complexity in the bargain. The `incomplete` branch is
caller-specific (open Sumsub vs keep polling) and stays inline.

Also switch `pollUntilApplyAdvances` to return `null` on timeout instead
of a synthetic `{ status: 'timeout' }` sentinel — the sentinel overlapped
the `ApplyForCardResponse` fallback branch and blocked TS narrowing.
Without an abort signal, an impatient user navigating away from the
pending screen during a slow Sumsub auto-review would burn up to 15
sequential apply requests over 15s — and trigger setState on an unmounted
component on the way out. The slow-Sumsub path is exactly when users get
twitchy, so this is the case the abort guard matters.

- pollUntilApplyAdvances accepts an optional AbortSignal and exits the
  loop on abort (no further fetches, no sleep tail, returns null).
- card/page.tsx creates an AbortController per poll, stores it in a ref,
  and aborts on unmount via useEffect cleanup. Post-await guards skip the
  setState calls if aborted.

Note: this aborts the loop, not the in-flight fetch (rainApi doesn't take
signals today). Worst-case wasted fetches drop from 15 to 1.
fix(card): unstick users post-Sumsub by polling instead of single re-apply
- ActivationCTAs: keep dev's iconBg + dismissable fields, keep main's useProviderRejectionStatus
- underMaintenance: keep dev's disableXchain naming + values, keep main's CROSS_CHAIN_DISABLED_MESSAGE
- content submodule: take main's updated submodule (delete-account help article)
- sumsub.ts: rewrite initiateSelfHealResubmission to use serverFetch
- Initial.view/Confirm.view: rename disableSquidSend → disableXchainSend
- ActivationCTAs: add iconBg to provider rejection overrides
- format: apply pnpm format
- blocked state: route to crisp support chat instead of opening KYC
  modal in manteca withdraw, add-money, and claim flows
- stale needsBridgeEnrollment: refetch user after KYC success in
  add-money/bank and withdraw/bank pages
- FINAL vs PROVIDER_FINAL: treat both as terminal blocked state in
  useProviderRejectionStatus and useQrKycGate
- useQrKycGate: add user?.rails to useCallback deps to fix stale closure
- ActivationCTAs: exclude card step from provider rejection override
- sumsub.ts: validate self-heal response fields before returning
chore: sync main into dev (conflicts resolved)
internal dev tool for visualizing the complete KYC flow — entry points,
sumsub verification, provider submission, self-heal resubmit pipeline,
cross-region flows, and post-approval UX. loads mermaid from CDN,
no npm dependency needed.
page.tsx is now a server component that reads flow-diagram.md from
mono at runtime. MermaidRenderer.tsx is the client component that
loads mermaid from CDN and renders the parsed blocks. single source
of truth — edit the markdown in mono, refresh to see changes.
Peanut no longer accepts SimpleFi as a QR payment processor. This strips
~900 lines of dead branching from the QR-pay page and the surrounding
plumbing (service, scanner, KYC gate, type narrowings).

What's removed:
- `services/simplefi.ts` (entire file — no callers left)
- The SIMPLEFI processor branch in `qr-pay/page.tsx`: state, handlers,
  WebSocket polling, success UI, retry path. Page drops from 1744 → ~840
  lines.
- `EQrType.SIMPLEFI_*` enum members + parser (`parseSimpleFiQr`, regex
  variants) in `DirectSendQR/utils.ts` + corresponding scanner cases and
  recognizer tests.
- `'MANTECA' | 'SIMPLEFI'` types narrowed to `'MANTECA'` in
  `useQrKycGate.ts`, `qr-payment.utils.ts`, `underMaintenance.config.ts`.
  The "skip KYC for SIMPLEFI" branch becomes unreachable.

What's kept (historical-data display):
- `EHistoryEntryType.SIMPLEFI_QR_PAYMENT` and the `simplefiQrPayment`
  transaction-display strategy + registry — old SimpleFi payments still
  show up correctly in users' transaction history.
- `assets/payment-apps` `SIMPLEFI` logo export — referenced by the
  history strategy.
- `utils/history.utils.ts` SIMPLEFI labelling — same reason.

Companion peanut-api-ts PR drops the matching backend dead-code stubs.
Hugo0 and others added 3 commits May 14, 2026 16:38
…E routes

Drift had accumulated since the last `pnpm gen:api` — backend had added 7
real routes that FE was calling via raw `serverFetch` strings (no type
safety). All 7 are now typed in `paths`:

- /rhino/bridge/{chains,commit,quote,status/{bridgeId}}
- /tokens/{price,wallet-portfolio}
- /users/identity/resubmit

Method: fetched current `/openapi.json` from staging, then merged in the
34 `/dev/*` paths from the previous FE snapshot (test-mode-only routes
the harness uses; staging strips them at deploy via `__INCLUDE_DEV_ROUTES__`).
No schema loss — both sides use inline TypeBox schemas, none in
`components.schemas`.

The big diff is reformatting churn from openapi-typescript regenerating
the whole file; the actual route additions are small.

Out of scope for this PR but worth a follow-up: automate this so it doesn't
drift again. Either a CI job that pulls the live spec and opens a PR, or a
peanut-api-ts deploy step that pushes the spec to peanut-ui.
…2024)

* feat(bug-bounty): home-carousel CTA + finish SUPPORT_SURVIVOR badge utils

Frontend half of the bug-bounty program (TASK-19358). Surfaces a CTA on
the home carousel that opens Crisp with a prefilled "I found a bug:"
message. Crispy (the support agent) classifies the report and calls the
new internal grant endpoint (separate peanut-api-ts PR) to send the
user $5 USDC and the SUPPORT_SURVIVOR badge.

## Changes

- **public/badges/support_survivor.svg** — vectorized badge asset from
  the approved v2 design. Pulled in from PR #1962 (closing that PR in
  favor of this one to ship the badge mapping + description + CTA as
  one cohesive frontend unit).
- **src/components/Badges/badge.utils.ts**
  - `CODE_TO_PATH.SUPPORT_SURVIVOR → /badges/support_survivor.svg`
  - `PUBLIC_DESCRIPTIONS.SUPPORT_SURVIVOR` — the third-person blurb
    rendered on /badges. (#1962 missed this; rolled in here.)
- **src/components/Global/Icons/Icon.tsx** — added Lucide `Bug` icon
  + `'bug'` name to the IconName union. Used by the new CTA.
- **src/hooks/useHomeCarouselCTAs.tsx** — bug-bounty CTA gated on
  `isActivated`. Click prefills Crisp with "I found a bug: " and opens
  the chat. Server enforces real eligibility (email verified, ≥1
  successful payment OR KYC approved, lifetime caps, daily budget) —
  the activation gate just hides it from cold accounts where the
  reward would be denied.

## Server side (separate PR)

The backend endpoint `POST /internal/support/grant` lives in
peanut-api-ts (TODO: open that PR next). The Crispy skill that calls
it lives in crispy (TODO: third PR). This PR is the user-visible
half — safe to ship standalone since the CTA only opens Crisp; no
backend dependency.

* feat(bug-bounty): rewire CTA to SupportDrawer + new copy + pink default + kyc icon

Tested locally via the mono harness (./scripts/dev on :3050) and three
things were wrong with the initial cut. Fixes from this round:

## 1. Click did nothing (root cause)

The CTA called `window.$crisp.push(...)`, the Crisp loader pattern.
That only works on `/[locale]/(marketing)/*` routes where the
`client.crisp.chat/l.js` script is injected by the marketing layout.
On `(mobile-ui)/home` Crisp lives inside `SupportDrawer`'s iframe and
the parent window has no `$crisp` global — pushes went to a queue
that never resolves.

Now uses the existing `openSupportWithMessage` helper from
`ModalsContext`, which sets `supportPrefilledMessage` + opens
`SupportDrawer`. Same path the other mobile-ui flows use to open
support (Manteca add-money, claim flow, withdraw, etc).

## 2. Copy

Title:       `Help us improve and get $5!`
Description: `Report a bug. Get rewarded! No questions asked.`

Original from Hugo's first ask: "found a bug, message us in support.
You'll get 5 bucks, 5 euros or 5 dollars, no questions asked."
Refined after a live test pass — drops the "earn $5 + a badge"
framing in favour of the simpler one-line bounty hook.

## 3. Background colour + icon

- Background: `bg-primary-1` (pink) instead of `bg-secondary-1`
  (yellow). Yellow was visually identical to qr-payment, ios-pwa-
  install, latam-cashback-invite, and kyc-prompt. Pink stands out
  and matches the "reward" framing already used by perks/invite.
- Bug icon bumped 16 → 20 to read at a glance.

## 4. KYC prompt icon

Swapped `shield` → `qr-code` on the "Unlock QR code payments" CTA.
The shield read as a security/verification cue rather than a
payments cue.

## Dev cheat

`?cheat=ctas` on `/home` bypasses every gate so we can compare all
carousel CTAs side by side. Pure local-iteration tool; will be
dropped before merge once we're done picking the colour.

## Routes/files

- src/hooks/useHomeCarouselCTAs.tsx — gate flips, copy, colour,
  icon, click handler rewire, cheat block

* refactor(carousel): simplify pass — useSearchParams + trim stale comment

Two reviewer findings actioned:

1. **Switch raw URLSearchParams → `useSearchParams` from `next/navigation`.**
   CLAUDE.md bans manual `URLSearchParams` parsing in favour of nuqs /
   the Next.js hook. For a one-off dev cheat read, useSearchParams is
   the lighter idiomatic fix (already importing useRouter from the
   same module). Same value, slightly less noisy + reacts to nav.

2. **Trim stale "three color variants" comment.** The previous round
   had three bug-bounty colour variants in cheat mode for side-by-
   side picking; I dropped two of them but left the comment that
   referenced them. Shortened to one line.

Skipped per reviewer guidance:
- NODE_ENV-gating the cheat — tree-shaking footgun in preview builds.
- `pushIf(gate, cta)` helper to dedupe the `cheatAllCTAs || (...)`
  prefix — would hide the gate next to the payload, break symmetry
  with the other 6 CTAs.
- Hoisting cheat eval out of useCallback — micro-allocation, below
  noise.

* chore(carousel): remove ?cheat=ctas dev helper before merge

CodeRabbit flagged the cheat as a prod surface — any user could append
?cheat=ctas to /home and see every CTA regardless of eligibility. The
CTAs themselves are non-destructive (just modal opens / navigation),
but it's noise + a small attack surface we don't need.

The cheat served its purpose: Hugo eyeballed yellow/pink/green/bare
side-by-side, picked pink, copy iterated, click handler rewired to
SupportDrawer. With the design locked, the cheat block + all the
`cheatAllCTAs || (...)` gate flips can go.

Reverted: imports, the const, all 7 gate patches, the deps array
entry. Functional change is zero — the bug-bounty CTA, the kyc-
prompt icon swap, and the openSupportWithMessage rewire all stay.

* fix(carousel): persist notification-prompt dismiss for 7 days like every other CTA

Original intent of TRANSIENT_CTA_IDS:

> CTAs gated by external state that can flip back (e.g. notification
> permission) must not be persisted — they should re-evaluate every
> session.

Sound on paper, wrong in practice. The 95% case is "user doesn't want
notifications right now" — under the old behaviour they got pinged
again every page reload until they finally gave up and granted, which
is annoying. The 5% case it was guarding against (user enables permission,
then revokes within 7 days, and wants the in-app prompt to re-appear
during that window) is rare enough that the cost-to-benefit doesn't
justify pestering everyone else every session.

With the cooldown in place:
- Dismiss → quiet for 7 days, same as every other CTA.
- Permission gates still fire — if `isPermissionGranted` flips true,
  the CTA hides regardless of dismiss record.
- If permission flips back from granted to denied within the 7-day
  window, the user just waits out the remaining days. Not catastrophic.

`TRANSIENT_CTA_IDS` had a single member ('notification-prompt'); with
it gone the set is empty so the whole mechanism dies. `dismissCTA` is
now a single straight path — always record, always persist, always
remove from current carousel.
Hugo0 and others added 3 commits May 14, 2026 16:45
These files are regenerated by `pnpm gen:api` (snapshot pulled from BE
staging /openapi.json). Letting prettier touch them creates pointless
diff churn on every regen — same reason the snapshot-bake fixtures
above are ignored.

Drops the format-check failure that just hit PR #2030.
… changed

This is the systemic fix for the drift the rest of this PR is patching by
hand. `pnpm gen:api` reads from a committed snapshot; nothing kept that
snapshot in sync with the live BE, so it drifted (we were 7 routes behind
when this PR was opened — type-safety on /rhino/bridge/*, /tokens/*, and
/users/identity/resubmit was silently absent).

## What

- `scripts/sync-openapi.mjs` — fetches a live OpenAPI spec, merges in the
  /dev/* paths from the existing FE snapshot (test-mode-only routes the
  Nutcracker harness uses; staging strips them at deploy), writes back to
  src/types/api.openapi.json. Designed to be idempotent: re-running with
  no BE change produces zero diff.
- `.github/workflows/sync-openapi.yml` — runs daily at 08:00 UTC weekdays
  (and on workflow_dispatch). Calls the script, runs `pnpm gen:api`, and
  if anything changed, opens an auto-PR against dev. Uses the stock
  GITHUB_TOKEN — no cross-repo auth, no PAT.
- Regenerated src/types/api.openapi.json + api.generated.ts via the new
  script so the committed snapshot matches what the workflow would
  produce. Without this, every workflow run would open a spurious "no-op"
  PR for the formatting-difference between the script and prettier.

## Why no peanut-api-ts change

OpenAPI is one-way: BE auto-generates /openapi.json from its Fastify route
schemas via `app.swagger()`. FE consumes it. The contract lives entirely
in the BE; FE just needs a fresh snapshot. Pulling beats pushing — no
secret coordination, no BE deploy modification, idempotent.

## Test plan

- [x] Script runs locally, output is idempotent against itself
- [x] `pnpm test` 52/52 green (1069 tests)
- [ ] After merge: workflow_dispatch the new workflow once to confirm it
      produces zero diff (idempotency check). Then leave it on the cron.
…apshot

chore(types): refresh openapi snapshot — type-safety for 7 new BE routes
Comment thread .github/workflows/sync-openapi.yml Dismissed
Hugo0 and others added 2 commits May 14, 2026 17:25
…Hog cross-link

Adds bi-directional linking between FE Sentry errors and PostHog user profiles:
- Each Sentry exception → `$exception` event in PostHog with a Sentry deeplink
- Each Sentry event → PostHog tag pointing back at the user + session replay

Pre-req: NEXT_PUBLIC_POSTHOG_KEY and NEXT_PUBLIC_SENTRY_DSN both set per env.
posthog.init() runs in instrumentation-client.ts; the integration captures the
singleton lazily, so load order doesn't matter.
…gration

feat(observability): wire posthog.sentryIntegration for Sentry ↔ PostHog cross-link
Hugo0 and others added 3 commits May 14, 2026 17:56
When the BE history fetcher can't attach a Peanut username to a counterparty,
the row currently renders as a truncated 0x address. This adds a lazy
JustaName `usePrimaryName` lookup at the row level so addresses upgrade to
their ENS primary name (e.g. peanuthelp.peanut.me, vitalik.eth) once the
async resolves.

Performance: the hook short-circuits when the field is already a username
(`isAddress(name)` guard), JustaName SWR-caches by address across rows, and
the synchronous render path is unchanged — addresses paint immediately and
upgrade in place when the lookup resolves.
…y-rows

feat(activity): lazy ENS reverse-lookup for unknown counterparties
Hugo0 and others added 2 commits May 14, 2026 18:22
Adds 'survivor' to INVITE_CODE_TO_CAMPAIGN_MAP so support agents can
share an invite link (e.g. /invite?code=survivor) that auto-awards
the SUPPORT_SURVIVOR badge on signup.

Pairs with peanut-api-ts whitelisting SUPPORT_SURVIVOR on /badge/award.
Badge-only — the $5 USDC bundle still requires the admin grant endpoint.
…vite-link

feat(invites): map 'survivor' invite code → SUPPORT_SURVIVOR badge
Hugo0 and others added 2 commits May 14, 2026 18:52
Triggers a fresh Vercel build for the dev branch so the updated
NEXT_PUBLIC_SENTRY_DSN scope ("All Environments") gets inlined into the
staging.peanut.me bundle. Vercel doesn't auto-rebuild on env-var scope
changes — the cached bundle from the previous build had dsn=undefined.

Inline comment captures the trap so the next person who edits Sentry
env vars knows to push a commit / clear build cache.
…uild

chore(sentry): force staging rebuild + document Vercel env-scope footgun
@Hugo0 Hugo0 merged commit 74f5c57 into main May 14, 2026
24 of 27 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants