Skip to content

feat(billing): /upgrade naar Stripe + Mijn Ribba met portal-endpoint - #32

Merged
onder-ates merged 4 commits into
mainfrom
feat/stripe-upgrade-wiring
Jul 21, 2026
Merged

feat(billing): /upgrade naar Stripe + Mijn Ribba met portal-endpoint#32
onder-ates merged 4 commits into
mainfrom
feat/stripe-upgrade-wiring

Conversation

@onder-ates

@onder-ates onder-ates commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Wat (na B5-correctieronde, commit dbacbbe)

De wiring-PR uit het vierstappenplan: de bewezen Stripe-keten aangesloten op de webclient. Geen nieuwe billing-, retry-, Customer-, webhook- of licentielogica, en — na de correctie — nul Stripe-secrets in Vercel: alle Stripe-runtime blijft binnen de projectbrede Supabase-secretset (B5).

  1. /upgrade → Stripe: handleCheckout roept stripe-create-checkout aan i.p.v. het Mollie-/api/checkout. Attempt-semantiek: één attempt_id per bewuste poging; netwerkfout/ambigue uitkomst → zelfde attempt_id hervat; ontvangen definitieve HTTP-fout → poging afgesloten, bewuste nieuwe klik = nieuwe attempt_id (Stripe cachet ook fouten op een idempotency-key); ander plan = eigen id. Dubbelklik geblokkeerd in controller én disabled-knop; Nederlandse fouten (servertekst met NL-fallback).
  2. Succespagina plan-neutraal: "Welkom bij Ribba! Je abonnement is geactiveerd." — een losse ?plan=-parameter of client-side opslag bepaalt niet meer wat als gekocht wordt gepresenteerd; een planlabel komt pas terug met een betrouwbare server-side bron.
  3. Mijn Ribba (/mijn-ribba + link op /upgrade): roept geauthenticeerd de ribbaPro edge function stripe-portal-session aan (PolderLabs/ribbaPro#266) met de user-JWT — zelfde patroon als checkout; de functie geeft { url } van een verse portal-sessie terug. Het eerdere /api/portal en lib/portal-session.ts zijn volledig verwijderd.

Drie poortvragen ("Stripe doet Stripe")

  1. Kan Stripe dit al zelf? Beheer volledig via de Portal — geen eigen beheer-UI. De brug (wie is ingelogd → eigen Customer) zit in de edge function; deze PR bevat alleen de dunne aanroep.
  2. Moet Ribba dit lokaal weten? De webclient slaat niets op en leest niets nieuws.
  3. Welk concreet foutscenario voorkomt deze code? (a) dubbelklik → geen tweede UI-poging; (b) vastzitten op een door Stripe gecachte fout na definitieve mislukking; (c) succes-tekst die een niet-geverifieerd plan claimt; (d) een tweede Stripe-secretbron buiten de atomair omschakelbare set.

Bewijs (npm test: 129/129 groen; next build slaagt)

GO-eis Test
Basic/Premium openen de juiste Stripe Checkout … roept stripe-create-checkout aan met school, plan en attempt_id
Dubbelklik → geen dubbele UI-poging dubbelklik: tweede begin() … geeft null
Netwerkfout → zelfde poging hervat netwerkfout (ambigue uitkomst): retry hervat DEZELFDE attempt_id
Definitieve fout → nieuwe poging, nieuwe id definitieve HTTP-fout sluit de poging af: nieuwe klik = NIEUWE attempt_id
Mijn Ribba voor de juiste school openStripePortal roept stripe-portal-session aan met JWT en school_id (eigenaarschap/modus/accountscope: getest in ribbaPro #266)
Geen actieve Customer → netjes portal: serverfout (bv. 409 geen koppeling) toont de Nederlandse servertekst
Succestekst plan-neutraal per besluit 21 jul
Mollie-bestanden geen diff netto diff = 5 bestanden, geen enkel Mollie-pad
Geen Stripe-secret in ribba-web repo-brede grep op STRIPE_SECRET/ACCOUNT/WEBHOOK/sk_/rk_ → 0 treffers

Volgorde

Eerst PolderLabs/ribbaPro#266 beoordelen (de edge function), daarna deze PR — Mijn Ribba werkt pas nadat #266 gemerged én gedeployed is.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added “Mijn Ribba” customer portal with login-protected account details and a billing/subscription management button that opens the Stripe billing portal.
  • Improvements
    • Refined the upgrade checkout flow to prevent duplicate checkout clicks and to recover correctly after returning from Stripe.
    • Simplified the upgrade success page to plan-neutral messaging.
    • Updated billing guidance/disclaimer to direct users to Mijn Ribba.

Bedrading van de bewezen Stripe-keten naar de webclient — geen nieuwe
billinglogica:

- /upgrade roept nu de edge function stripe-create-checkout aan
  (attempt_id per bewuste poging, hergebruik bij retry van hetzelfde
  plan, dubbelklik-blokkade in controller én knop, Nederlandse fouten);
- /upgrade/success toont het echte plan (URL-param voor Mollie,
  sessionStorage voor Stripe; onbekend -> neutrale tekst, nooit stil
  'Premium');
- POST /api/portal maakt met Ribba-auth een verse Stripe Customer
  Portal-sessie op de bestaande stripe_customers-koppeling (fail-closed
  keyprefix, objectbewijs op session.livemode, no_customer -> 409 met
  duidelijke vervolgstap);
- /mijn-ribba: minimale permanente klantportaalpagina met één actie.

Mollie-paden (/api/checkout, /api/cancel-subscription, mollie-webhook)
volledig onaangeroerd; bestaande Mollie-klanten blijven ongewijzigd
werken. Tests: 136/136 groen (23 nieuw).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 21, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
ribba-pro Ready Ready Preview, Comment Jul 21, 2026 10:29am

Request Review

@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

No new commits to review since the last review.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 144f6f13-33d3-4692-9bcb-331f8a0f145e

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The PR adds an authenticated Mijn Ribba portal page, introduces Stripe portal and checkout Edge Function helpers with concurrency and retry handling, updates the upgrade flow to use those helpers, and makes the success page plan-neutral. Tests cover request wiring, validation, errors, and gating.

Changes

Stripe billing flows

Layer / File(s) Summary
Checkout controller and request helper
lib/stripe-upgrade.ts, tests/stripe-upgrade.test.mjs
Checkout attempts use per-plan identifiers, concurrent-attempt protection, Edge Function requests, URL validation, and typed network or definitive errors covered by tests.
Authenticated customer portal flow
lib/stripe-upgrade.ts, app/mijn-ribba/page.tsx, tests/stripe-upgrade.test.mjs
Mijn Ribba authenticates users, loads school data, prevents overlapping portal requests, calls the Stripe portal function, displays errors, and redirects to the returned portal URL.
Upgrade checkout and success-page integration
app/upgrade/page.tsx, app/upgrade/success/page.tsx
The upgrade page uses the checkout controller and helper, links billing management to Mijn Ribba, and renders plan-neutral activation copy.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

Upgrade checkout

sequenceDiagram
  participant UpgradePage
  participant CheckoutController
  participant startStripeCheckout
  participant StripeCheckoutFunction
  UpgradePage->>CheckoutController: Begin plan attempt
  UpgradePage->>startStripeCheckout: Send checkout request
  startStripeCheckout->>StripeCheckoutFunction: POST with Bearer token
  StripeCheckoutFunction-->>startStripeCheckout: Return checkoutUrl or error
  startStripeCheckout-->>UpgradePage: Return checkout result
Loading

Mijn Ribba portal

sequenceDiagram
  participant Browser
  participant MijnRibbaPage
  participant SupabaseAuth
  participant openStripePortal
  participant StripePortalFunction
  Browser->>MijnRibbaPage: Open portal page
  MijnRibbaPage->>SupabaseAuth: Read session
  MijnRibbaPage->>openStripePortal: Send school_id and access token
  openStripePortal->>StripePortalFunction: POST portal request
  StripePortalFunction-->>openStripePortal: Return portal URL or error
  openStripePortal-->>MijnRibbaPage: Return portal result
  MijnRibbaPage->>Browser: Navigate to portal URL
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately captures the main change: moving billing/upgrade flow to Stripe and adding the Mijn Ribba portal endpoint.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/stripe-upgrade-wiring

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/upgrade/page.tsx (1)

163-213: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

handleCheckout never releases the controller's inFlight lock outside the explicit failure branch.

Two exit paths leave checkoutControllerRef.current permanently locked for this component instance:

  • Lines 185-188: if token is missing, the function returns without calling fail(plan).
  • Lines 206-213: on success, nothing releases inFlight before/while redirecting via window.location.href.

Because this is a full-page window.location.href navigation (often cross-origin, to Stripe), the browser's back-forward cache can restore this exact component/ref state when the user returns via the back button — at which point begin() silently returns null forever, and clicking any plan button does nothing with no visible error. See the consolidated comment (anchored on lib/stripe-upgrade.ts) for the suggested complete() API and the corresponding call-site fix here.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/upgrade/page.tsx` around lines 163 - 213, Update handleCheckout to
release checkoutControllerRef.current’s inFlight lock on every exit after
begin(plan), including the missing-token branch and the successful checkout path
before window.location.href navigation. Use the controller’s suggested
complete() API for successful attempts and retain fail(plan) for failed
attempts, ensuring back-forward-cache restoration allows a new checkout attempt.
🧹 Nitpick comments (5)
app/api/portal/route.ts (1)

78-88: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Add a timeout to the Stripe Billing Portal call.

The fetch to Stripe has no timeout, so a slow/hung upstream ties up the request until the platform limit. Wrap it with AbortSignal.timeout(...) and handle the abort in the existing catch.

♻️ Suggested timeout guard
     const stripeRes = await fetch('https://api.stripe.com/v1/billing_portal/sessions', {
       method: 'POST',
       headers: {
         Authorization: `Bearer ${stripeKey}`,
         'Content-Type': 'application/x-www-form-urlencoded',
       },
       body: new URLSearchParams({
         customer: decision.stripeCustomerId,
         return_url: returnUrl,
       }),
+      signal: AbortSignal.timeout(10_000),
     });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/api/portal/route.ts` around lines 78 - 88, Add a timeout to the Stripe
Billing Portal request in the fetch call by passing an AbortSignal.timeout
value, and ensure the existing catch handles timeout aborts consistently with
other request failures. Keep the current request payload and response handling
unchanged.
app/upgrade/page.tsx (1)

166-166: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Minor: createCheckoutController() is invoked on every render.

useRef(createCheckoutController()) evaluates the argument on every re-render even though useRef only keeps the first value. The controller is cheap to construct, so this is negligible, but a lazy pattern avoids the redundant allocation.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/upgrade/page.tsx` at line 166, Update the checkoutControllerRef
initialization in the upgrade page to lazily create the controller only when the
ref has no current value, rather than evaluating createCheckoutController() on
every render. Preserve the existing controller instance across subsequent
renders.
app/upgrade/success/page.tsx (1)

15-22: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Stale plan label on repeat visits.

UPGRADE_PLAN_STORAGE_KEY is read but never cleared, so if this success page is revisited later in the same tab session without a fresh ?plan=, it could still show the previous purchase's plan label. Low priority since urlPlan wins when present (Mollie flow) and sessionStorage is tab-scoped, but consider sessionStorage.removeItem(UPGRADE_PLAN_STORAGE_KEY) after reading to avoid staleness on a second visit within the Stripe flow.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/upgrade/success/page.tsx` around lines 15 - 22, Clear
UPGRADE_PLAN_STORAGE_KEY from sessionStorage immediately after reading it in the
success-page effect, while preserving the existing error handling for blocked
storage and the successPlanLabel call using the retrieved value.
lib/stripe-upgrade.ts (1)

70-111: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

No timeout/abort on the checkout fetch.

If the edge function hangs, startStripeCheckout will wait indefinitely; combined with the controller-lock issue above, the button stays disabled/"Bezig..." with no escape until the user reloads. Consider wiring an AbortController-based timeout (a few seconds) so a hang degrades to a retryable error instead of an indefinite spinner.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/stripe-upgrade.ts` around lines 70 - 111, The startStripeCheckout fetch
can hang indefinitely and leave the checkout flow stuck. Add an
AbortController-based timeout around the doFetch call in startStripeCheckout,
pass its signal to the request, and convert timeout aborts into the existing
retryable checkout error while preserving current network and response handling.
tests/stripe-upgrade.test.mjs (1)

21-48: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

No test covers the success-then-reattempt / stuck-inFlight scenario.

Given the gap flagged in lib/stripe-upgrade.ts (no way to release inFlight after a successful begin()), consider adding a test asserting that after a successful checkout flow, a subsequent begin() for the same or a different plan is not permanently blocked. This would lock in the fix once complete()/reset is added.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/stripe-upgrade.test.mjs` around lines 21 - 48, The checkout controller
tests cover duplicate attempts and failure retries but not releasing inFlight
after success. Add a test using createCheckoutController that begins a plan,
completes or resets the successful attempt through the exposed API, then
verifies a subsequent begin for the same or another plan is accepted and
produces the expected attempt_id.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/mijn-ribba/page.tsx`:
- Around line 32-52: Update the getSession promise chain in the page component
to add a rejection handler that calls setLoading(false) and surfaces the
authentication/session error using the component’s existing error-handling
mechanism. Keep the inner try/finally behavior unchanged for successful session
retrieval and API failures.
- Line 123: Replace the raw anchor for the internal /upgrade navigation in the
page component with Next.js’s Link component, importing next/link as needed
while preserving the existing link text and surrounding markup.

In `@lib/stripe-upgrade.ts`:
- Around line 16-48: Extend the CheckoutController API and
createCheckoutController with a completion/reset method that clears inFlight and
removes the plan’s cached attempt ID. Update app/upgrade/page.tsx’s
handleCheckout to invoke this method both when checkout succeeds before
redirecting and when the access token is missing before the network request,
while retaining fail() for request errors.

---

Outside diff comments:
In `@app/upgrade/page.tsx`:
- Around line 163-213: Update handleCheckout to release
checkoutControllerRef.current’s inFlight lock on every exit after begin(plan),
including the missing-token branch and the successful checkout path before
window.location.href navigation. Use the controller’s suggested complete() API
for successful attempts and retain fail(plan) for failed attempts, ensuring
back-forward-cache restoration allows a new checkout attempt.

---

Nitpick comments:
In `@app/api/portal/route.ts`:
- Around line 78-88: Add a timeout to the Stripe Billing Portal request in the
fetch call by passing an AbortSignal.timeout value, and ensure the existing
catch handles timeout aborts consistently with other request failures. Keep the
current request payload and response handling unchanged.

In `@app/upgrade/page.tsx`:
- Line 166: Update the checkoutControllerRef initialization in the upgrade page
to lazily create the controller only when the ref has no current value, rather
than evaluating createCheckoutController() on every render. Preserve the
existing controller instance across subsequent renders.

In `@app/upgrade/success/page.tsx`:
- Around line 15-22: Clear UPGRADE_PLAN_STORAGE_KEY from sessionStorage
immediately after reading it in the success-page effect, while preserving the
existing error handling for blocked storage and the successPlanLabel call using
the retrieved value.

In `@lib/stripe-upgrade.ts`:
- Around line 70-111: The startStripeCheckout fetch can hang indefinitely and
leave the checkout flow stuck. Add an AbortController-based timeout around the
doFetch call in startStripeCheckout, pass its signal to the request, and convert
timeout aborts into the existing retryable checkout error while preserving
current network and response handling.

In `@tests/stripe-upgrade.test.mjs`:
- Around line 21-48: The checkout controller tests cover duplicate attempts and
failure retries but not releasing inFlight after success. Add a test using
createCheckoutController that begins a plan, completes or resets the successful
attempt through the exposed API, then verifies a subsequent begin for the same
or another plan is accepted and produces the expected attempt_id.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 232fdc99-21ea-4829-afe0-fd00373448f2

📥 Commits

Reviewing files that changed from the base of the PR and between d12a1f5 and 3469e50.

📒 Files selected for processing (8)
  • app/api/portal/route.ts
  • app/mijn-ribba/page.tsx
  • app/upgrade/page.tsx
  • app/upgrade/success/page.tsx
  • lib/portal-session.ts
  • lib/stripe-upgrade.ts
  • tests/portal-session.test.mjs
  • tests/stripe-upgrade.test.mjs

Comment thread app/mijn-ribba/page.tsx
Comment on lines +32 to +52
supabase.auth.getSession().then(async ({ data }) => {
if (!data.session) {
router.replace('/login');
return;
}
setUserEmail(data.session.user.email ?? null);
try {
const meRes = await fetch('/api/me', {
headers: { Authorization: `Bearer ${data.session.access_token}` },
});
if (meRes.ok) {
const me = await meRes.json();
if (me.school_id) setSchoolId(me.school_id);
if (me.school_name) setSchoolName(me.school_name);
}
} catch {
// foutafhandeling hieronder via ontbrekende schoolId
} finally {
setLoading(false);
}
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

getSession() rejection leaves the page stuck on "Laden…".

The .then(...) chain has no rejection handler, and setLoading(false) runs only inside the inner try/finally. If getSession() itself rejects (network/session error), loading never clears and the user sees an indefinite spinner. Add a .catch that clears loading (and surfaces an error).

🛡️ Suggested guard
-    supabase.auth.getSession().then(async ({ data }) => {
+    supabase.auth.getSession().then(async ({ data }) => {
       if (!data.session) {
         router.replace('/login');
         return;
       }
       setUserEmail(data.session.user.email ?? null);
       try {
         const meRes = await fetch('/api/me', {
           headers: { Authorization: `Bearer ${data.session.access_token}` },
         });
         if (meRes.ok) {
           const me = await meRes.json();
           if (me.school_id) setSchoolId(me.school_id);
           if (me.school_name) setSchoolName(me.school_name);
         }
       } catch {
         // foutafhandeling hieronder via ontbrekende schoolId
       } finally {
         setLoading(false);
       }
-    });
+    }).catch(() => setLoading(false));
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
supabase.auth.getSession().then(async ({ data }) => {
if (!data.session) {
router.replace('/login');
return;
}
setUserEmail(data.session.user.email ?? null);
try {
const meRes = await fetch('/api/me', {
headers: { Authorization: `Bearer ${data.session.access_token}` },
});
if (meRes.ok) {
const me = await meRes.json();
if (me.school_id) setSchoolId(me.school_id);
if (me.school_name) setSchoolName(me.school_name);
}
} catch {
// foutafhandeling hieronder via ontbrekende schoolId
} finally {
setLoading(false);
}
});
supabase.auth.getSession().then(async ({ data }) => {
if (!data.session) {
router.replace('/login');
return;
}
setUserEmail(data.session.user.email ?? null);
try {
const meRes = await fetch('/api/me', {
headers: { Authorization: `Bearer ${data.session.access_token}` },
});
if (meRes.ok) {
const me = await meRes.json();
if (me.school_id) setSchoolId(me.school_id);
if (me.school_name) setSchoolName(me.school_name);
}
} catch {
// foutafhandeling hieronder via ontbrekende schoolId
} finally {
setLoading(false);
}
}).catch(() => setLoading(false));
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/mijn-ribba/page.tsx` around lines 32 - 52, Update the getSession promise
chain in the page component to add a rejection handler that calls
setLoading(false) and surfaces the authentication/session error using the
component’s existing error-handling mechanism. Keep the inner try/finally
behavior unchanged for successful session retrieval and API failures.

Comment thread app/mijn-ribba/page.tsx
<div className="divider" />

<p className="footer-text">
Abonnement kiezen of wijzigen? <a href="/upgrade">Bekijk de plannen</a>.<br />

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Use next/link for internal navigation to /upgrade.

The raw <a href="/upgrade"> triggers a full page reload instead of client-side navigation.

♻️ Suggested change
-import { useRouter } from 'next/navigation';
+import { useRouter } from 'next/navigation';
+import Link from 'next/link';
-              Abonnement kiezen of wijzigen? <a href="/upgrade">Bekijk de plannen</a>.<br />
+              Abonnement kiezen of wijzigen? <Link href="/upgrade">Bekijk de plannen</Link>.<br />
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Abonnement kiezen of wijzigen? <a href="/upgrade">Bekijk de plannen</a>.<br />
Abonnement kiezen of wijzigen? <Link href="/upgrade">Bekijk de plannen</Link>.<br />
🧰 Tools
🪛 ESLint

[error] 123-123: Do not use an <a> element to navigate to /upgrade/. Use <Link /> from next/link instead. See: https://nextjs.org/docs/messages/no-html-link-for-pages

(@next/next/no-html-link-for-pages)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/mijn-ribba/page.tsx` at line 123, Replace the raw anchor for the internal
/upgrade navigation in the page component with Next.js’s Link component,
importing next/link as needed while preserving the existing link text and
surrounding markup.

Source: Linters/SAST tools

Comment thread lib/stripe-upgrade.ts
…cret in Vercel

Correctieronde op de wiring-PR na review:

- /api/portal en lib/portal-session.ts volledig verwijderd; de
  beslislogica leeft nu in de ribbaPro edge function
  stripe-portal-session (PR #266) binnen de projectbrede secretset;
- Mijn Ribba roept de edge function direct geauthenticeerd aan met de
  user-JWT (openStripePortal-helper, zelfde patroon als checkout);
- succestekst plan-neutraal: een losse ?plan=-parameter of client-side
  opslag mag niet bepalen wat als gekocht wordt gepresenteerd;
- attempt-semantiek aangescherpt: netwerkfout/ambigue uitkomst hervat
  dezelfde attempt_id; ontvangen definitieve HTTP-fout sluit de poging
  af en een bewuste nieuwe klik krijgt een nieuwe attempt_id (Stripe
  cachet ook fouten op een idempotency-key);
- tests bijgewerkt: 129/129 groen; Mollie-paden onveranderd zonder
  diff; nul Stripe-secretreferenties in de hele codebase.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
portalBusy + disabled is UI-bescherming, geen harde garantie: twee
click-events kunnen dezelfde render-state zien. createPortalGate is een
synchrone in-flight lock (ref-patroon, zelfde principe als de
checkout-controller): de eerste aanroep verkrijgt de lock en start één
request, een tweede stopt vóór getSession/fetch, finally geeft de lock
vrij. Bewust géén attempt_id op het portal-pad — een portal-sessie kent
geen idempotente resource; de gate bewaakt uitsluitend het aantal
requests.

Tests: dubbel-aanroep levert exact één Edge Function-request; gate komt
vrij na definitieve fout én na netwerkfout; na terugkeer kan bewust een
nieuwe sessie starten. 19/19 groen, tsc schoon.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@onder-ates

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Na een geslaagde checkout blijft de in-flight lock bewust staan tot de
redirect naar Stripe. Keert de gebruiker via browser-back terug, dan
herstelt de browser de pagina uit de back/forward-cache mét die actieve
lock — de checkoutknoppen waren dan permanent dood (CodeRabbit-Major).

Correctie: expliciete reset() op de controller die uitsluitend de lock
vrijgeeft; de upgradepagina registreert een pageshow-handler die alléén
bij event.persisted === true reset() aanroept en de loading-state wist,
met listener-cleanup in de effect-teardown. De attempt_id-semantiek is
onaangetast: een bfcache-terugkeer is geen serverantwoord, dus er wordt
niets afgesloten of vernieuwd — een nieuwe klik op hetzelfde plan hervat
dezelfde idempotente poging.

Vijf checkouttests in paginacompositie: dubbelklik -> exact één request;
succes vóór redirect -> lock actief; bfcache-terugkeer -> lock hersteld
en bewuste klik mogelijk (zelfde id); definitieve fout -> lock vrij +
nieuwe attempt_id; netwerkfout -> lock vrij + zelfde attempt_id.
24/24 groen, tsc schoon.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@onder-ates

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@onder-ates

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@onder-ates

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant