Skip to content

feat(stripe): premium account purchase by card on the web (P6d)#990

Merged
feruzm merged 4 commits into
developfrom
feature/stripe-premium-web
Jun 22, 2026
Merged

feat(stripe): premium account purchase by card on the web (P6d)#990
feruzm merged 4 commits into
developfrom
feature/stripe-premium-web

Conversation

@feruzm

@feruzm feruzm commented Jun 22, 2026

Copy link
Copy Markdown
Member

P6d (web) of the Stripe-on-web work: buy a premium Hive account with a card on the signup page, instead of only the mobile-IAP QR. Depends on the merged vapi #33 (anonymous routes + Turnstile) and ePoints #22 (the accounts rail).

Flow

The premium page already collects + validates username/email/referral. When isStripeEnabled() it now:

  1. renders the Turnstile widget and gates submit on it;
  2. on submit → StripeAccountCheckout, which mints a PaymentIntent via the anonymous vapi route /private-api/stripe-account-intent (no validateCode; sends sku=299accounts, a per-checkout nonce, {username,email,referral}, and the single-use captcha token);
  3. confirms with the reused StripeCheckoutForm / <Elements>;
  4. polls /private-api/stripe-account-status (by username + payment_intent) → terminal "account requested — check your email for your keys."

Keys + the on-chain account are created async by the onboard service, so "done" means requested, not delivered. When Stripe is disabled the QR/IAP fallback is unchanged.

Notes

  • The intent is minted once on mount — guarded for React StrictMode (a dedicated mount-ref, so the result isn't swallowed) and the single-use token (never re-spent). On error/back the page resets the Turnstile for a fresh token.
  • Web-only, gated behind isStripeEnabled. No new secret wiring — the Turnstile sitekey has a build fallback and the Stripe publishable key is already NEXT_PUBLIC.
  • The server (vapi Persist notifications sorting #33 + ePoints Fixed notifications mobile layout #22) re-validates the username (consensus syntax + on-chain availability), verifies the captcha, and computes the amount before charging.

Review

Adversarial review = GO-with-fixes; fixed: a StrictMode-remount hang (now a dedicated mount-ref), and 2 display nits (em-dash → dash, price from the shared STRIPE_ACCOUNT_USD constant). Full typecheck/lint/build left to CI.

Summary by CodeRabbit

  • New Features

    • Premium signup now supports direct card payments via Stripe with security verification
    • Added ability to resume and complete payments from redirect-based payment flows
    • Improved payment status tracking with clearer messaging for different stages and errors
  • Documentation

    • Added translations for payment flow states and error messages

The premium signup page now offers a Stripe card flow when the publishable key is
configured, instead of only the mobile-IAP QR. The buyer is anonymous (no Hive account
yet): a Cloudflare Turnstile token gates create-intent, which goes through the anonymous
vapi route (verified server-side) to ePoints (which re-validates the username + computes
the amount before charging).

- use-stripe-account-purchase: anonymous create-intent (sku 299accounts + captcha token
  + meta) + status poll (by username + payment_intent).
- stripe-account-checkout: mints the intent ONCE on mount (single-use-token + StrictMode
  safe via a dedicated mount-ref), confirms with the reused StripeCheckoutForm/Elements,
  polls, terminal 'account requested - check your email for your keys' (keys are emailed
  async by the onboard service).
- premium page: adds the Turnstile widget, gates submit on it, routes to the card flow
  when Stripe is enabled, keeps the QR/IAP as the fallback, resets the Turnstile on back.

Web-only, gated behind isStripeEnabled. No new secret wiring (sitekey fallback + the
NEXT_PUBLIC publishable key are already available to the build).

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: db78ae5acf

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

tries += 1;
try {
const st = await fetchStripeAccountStatus(meta.username, paymentIntentId);
if (!pollingRef.current) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Stop swallowing every account status response

When the post-payment poll receives any response, this undefined pollingRef reference throws before st.status is inspected, and the surrounding catch treats that as a transient polling failure. As a result the account checkout ignores both success and failed statuses and, after ~40 seconds, falls through to setStep("done"), so a failed account request can be shown to the buyer as requested instead of surfacing the delivery error.

Useful? React with 👍 / 👎.

… await

Leftover from the StrictMode mount-ref refactor: the post-await guard still named
pollingRef, which no longer exists. Use mountedRef so the poll bails cleanly when the
component unmounts mid-request.
@coderabbitai

coderabbitai Bot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@feruzm, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 16 minutes and 17 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan refill rate.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, the refill rate gradually slows as usage increases. The highest same-day bursts are limited more strictly.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ff82dacc-ac2b-4270-bfad-3e99e78a2d6b

📥 Commits

Reviewing files that changed from the base of the PR and between 17cf8bf and 4f1e52a.

📒 Files selected for processing (4)
  • .github/workflows/master.yml
  • .github/workflows/staging.yml
  • apps/web/docker-compose.production.yml
  • apps/web/docker-compose.yml
📝 Walkthrough

Walkthrough

Adds a Stripe-based in-page payment flow to the premium signup page, gated by Cloudflare Turnstile CAPTCHA. Introduces useCreateAccountIntent and fetchStripeAccountStatus API hooks, a new StripeAccountCheckout component with a multi-step state machine, redirect-resume handling for Stripe redirect-based payment methods, and eight new i18n strings covering all checkout states.

Changes

Stripe Account Checkout Flow

Layer / File(s) Summary
Types, constants, and API hooks
apps/web/src/features/shared/purchase-stripe/use-stripe-account-purchase.ts, apps/web/src/features/shared/purchase-stripe/index.ts
Exports STRIPE_ACCOUNT_SKU, STRIPE_ACCOUNT_USD, TypeScript interfaces for purchase metadata and account status, useCreateAccountIntent mutation (POST /private-api/stripe-account-intent), and fetchStripeAccountStatus polling helper (POST /private-api/stripe-account-status); module barrel re-exports both new modules.
StripeAccountCheckout component
apps/web/src/features/shared/purchase-stripe/stripe-account-checkout.tsx, apps/web/src/features/i18n/locales/en-US.json
Implements the creating → pay → delivering → done/error state machine with StrictMode-safe PaymentIntent minting, redirect-resume shortcut, ~2s delivery polling with max-retry cap and unmount guards, Stripe Elements rendering, and error display. Eight new i18n strings cover all step states and failure modes.
Premium signup page integration
apps/web/src/app/signup/premium/_page.tsx
Adds showCheckout, captchaToken, resumePi, resumeUsername state and Turnstile/Stripe refs; a mount effect parses and clears Stripe redirect URL params to resume or surface errors; handleSubmit branches on stripeEnabled to enter checkout or fall back to QR flow; form conditionally renders Turnstile widget, disables submit until captcha is solved, and shows price from STRIPE_ACCOUNT_USD.

Sequence Diagram(s)

sequenceDiagram
  actor User
  participant SignupPage
  participant Turnstile
  participant StripeAccountCheckout
  participant Server

  User->>SignupPage: fill form + solve CAPTCHA
  Turnstile-->>SignupPage: captchaToken
  User->>SignupPage: submit
  SignupPage->>StripeAccountCheckout: render(meta, captchaToken)
  StripeAccountCheckout->>Server: POST /private-api/stripe-account-intent
  Server-->>StripeAccountCheckout: client_secret
  StripeAccountCheckout->>User: render Stripe Elements (pay step)
  User->>StripeAccountCheckout: confirm payment
  StripeAccountCheckout->>StripeAccountCheckout: step → delivering
  loop poll every ~2s
    StripeAccountCheckout->>Server: POST /private-api/stripe-account-status
    Server-->>StripeAccountCheckout: StripeAccountStatus
  end
  StripeAccountCheckout->>User: done / error
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • ecency/vision-next#910: Both PRs integrate Cloudflare Turnstile CAPTCHA into signup flows — this PR gates the Stripe premium checkout submit on a captchaToken using the same Turnstile components.
  • ecency/vision-next#984: Both PRs extend the same purchase-stripe/index.ts barrel with new Stripe purchase module re-exports (stripe-account-checkout/use-stripe-account-purchase here vs. stripe-points-dialog/use-stripe-points-purchase there).
  • ecency/vision-next#986: The new StripeAccountCheckout depends on StripeCheckoutForm/PaymentElement directly updated in that PR, and its wallet initialization is affected by the Permissions-Policy header change there.

Poem

🐇 Hop, hop, through the payment gate,
A Turnstile solved, no time to wait!
The card is charged, the intent mints,
The rabbit polls for status hints.
delivering → done with a joyful leap —
Your premium account is yours to keep! 🎉

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 60.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(stripe): premium account purchase by card on the web (P6d)' accurately and concisely summarizes the main feature being implemented—Stripe card payment support for premium account purchases on the web platform.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/stripe-premium-web

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 and usage tips.

@greptile-apps

greptile-apps Bot commented Jun 22, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds Stripe card payment support to the premium account signup page, gated behind isStripeEnabled(). When enabled, a Cloudflare Turnstile challenge replaces the mobile-app QR/IAP fallback and a new StripeAccountCheckout component mints a PaymentIntent, collects card details via Stripe's Payment Element, and polls the anonymous vapi account-status endpoint until the worker records the account request.

  • New StripeAccountCheckout component (stripe-account-checkout.tsx): carefully guards single-use captcha spending with startedRef, handles React StrictMode double-invoke via a standalone mountedRef effect, and correctly bails all async state-setters after unmount.
  • Redirect-resume flow added to _page.tsx: reads payment_intent + u from the URL on mount, clears Stripe's params via history.replaceState, and passes a resumePaymentIntent prop to skip intent minting and jump straight to delivery polling.
  • CI/Docker wiring: TURNSTILE_SECRET is added to all three production-region workflows, staging, and both compose files.

Confidence Score: 5/5

Safe to merge — the card payment path is well-guarded behind a feature flag, the StrictMode/single-use token logic is correct, and the redirect-resume flow properly covers the only path where page state is lost on navigation.

The intent-creation guard (startedRef), the mounted-ref pattern (a standalone effect so StrictMode's cleanup→re-mount cycle leaves it true on the final mount), and the delivery-polling unmount guards all look correct. Previously flagged issues have been resolved. Remaining notes are style-level.

No files require special attention. stripe-account-checkout.tsx and _page.tsx carry the most logic and both read cleanly.

Important Files Changed

Filename Overview
apps/web/src/features/shared/purchase-stripe/stripe-account-checkout.tsx New component: mints a PaymentIntent once on mount (StrictMode-safe via startedRef + mountedRef), renders Stripe Payment Element, and polls delivery status. Redirect-resume path handled correctly. Minor: "creating" step exposes no back/cancel escape.
apps/web/src/features/shared/purchase-stripe/use-stripe-account-purchase.ts New file: mutation hook for PaymentIntent creation and a polling helper for account-order status. Anonymous (no validateCode). Constants, types, and API helpers are clean and well-commented.
apps/web/src/app/signup/premium/_page.tsx Adds Stripe card path behind isStripeEnabled(), Turnstile widget, redirect-resume detection, and StripeAccountCheckout rendering. On redirect-resume, email/referral state is empty but only meta.username is needed for polling, so functionally correct.
apps/web/src/features/i18n/locales/en-US.json Adds 7 new translation keys for the account purchase flow. All keys are scoped under sign-up.*. No existing keys changed.
.github/workflows/master.yml Adds TURNSTILE_SECRET to env/envs for all three production regions (EU, US, SG). Consistent with the existing secret-injection pattern.
.github/workflows/staging.yml Adds TURNSTILE_SECRET to the staging deployment. Mirrors the master workflow change.
apps/web/docker-compose.production.yml Adds TURNSTILE_SECRET to the production container environment list. No other changes.
apps/web/docker-compose.yml Adds TURNSTILE_SECRET to the dev container environment list. Consistent.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant U as User (Browser)
    participant P as PremiumSignUp page
    participant CF as Cloudflare Turnstile
    participant SAC as StripeAccountCheckout
    participant V as vapi (anon)
    participant S as Stripe
    participant E as ePoints

    U->>P: Fill username / email / referral
    P->>CF: Render widget
    CF-->>P: onVerify(captchaToken)
    U->>P: Submit form
    P->>P: Client-validate username (on-chain)
    P->>SAC: Mount (meta, captchaToken)
    SAC->>V: POST /stripe-account-intent (sku, nonce, meta, captcha)
    V->>CF: Verify captcha
    V->>E: Create PaymentIntent
    E-->>V: client_secret
    V-->>SAC: client_secret
    SAC->>S: confirmPayment (Payment Element)
    S-->>SAC: "paymentIntent.status = succeeded"
    SAC->>SAC: "step = delivering"
    loop Poll every 2s up to 20x
        SAC->>V: POST /stripe-account-status
        V-->>SAC: status pending/success/failed
    end
    SAC-->>U: Account requested - check email

    note over P,SAC: Redirect-APM path (edge case)
    S-->>U: Redirect to returnUrl with payment_intent params
    U->>P: Browser returns to page
    P->>P: Detect payment_intent in URL
    P->>SAC: Mount with resumePaymentIntent
    SAC->>SAC: "step = delivering (skip minting)"
    SAC->>V: POST /stripe-account-status (poll)
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant U as User (Browser)
    participant P as PremiumSignUp page
    participant CF as Cloudflare Turnstile
    participant SAC as StripeAccountCheckout
    participant V as vapi (anon)
    participant S as Stripe
    participant E as ePoints

    U->>P: Fill username / email / referral
    P->>CF: Render widget
    CF-->>P: onVerify(captchaToken)
    U->>P: Submit form
    P->>P: Client-validate username (on-chain)
    P->>SAC: Mount (meta, captchaToken)
    SAC->>V: POST /stripe-account-intent (sku, nonce, meta, captcha)
    V->>CF: Verify captcha
    V->>E: Create PaymentIntent
    E-->>V: client_secret
    V-->>SAC: client_secret
    SAC->>S: confirmPayment (Payment Element)
    S-->>SAC: "paymentIntent.status = succeeded"
    SAC->>SAC: "step = delivering"
    loop Poll every 2s up to 20x
        SAC->>V: POST /stripe-account-status
        V-->>SAC: status pending/success/failed
    end
    SAC-->>U: Account requested - check email

    note over P,SAC: Redirect-APM path (edge case)
    S-->>U: Redirect to returnUrl with payment_intent params
    U->>P: Browser returns to page
    P->>P: Detect payment_intent in URL
    P->>SAC: Mount with resumePaymentIntent
    SAC->>SAC: "step = delivering (skip minting)"
    SAC->>V: POST /stripe-account-status (poll)
Loading

Reviews (3): Last reviewed commit: "ci(stripe): wire TURNSTILE_SECRET into t..." | Re-trigger Greptile

Comment thread apps/web/src/app/signup/premium/_page.tsx
…tail

Addresses the PR review:
- (greptile P1) A redirect-based payment method returns to a fresh page load with empty
  form state, so the checkout never remounted and a charged buyer saw a blank form. Only
  reachable if a redirect-only APM is enabled (card + wallets + 3DS all confirm in-page via
  redirect:if_required), but a charge-then-blank-form is the worst outcome for an anonymous
  buyer, so resume it defensively. Mirror the Points resume convention: carry the (public)
  username on returnUrl (?u=), and on the premium page mount read payment_intent +
  redirect_status + u and resume straight into the delivery poll for the already-created
  order. No re-mint and no re-spend of the single-use captcha token (the mint effect latches
  startedRef and bails when resuming); failed/canceled returns surface an error toast.
- (greptile P2) poll()'s tries>=20 / reschedule tail ran without re-checking mountedRef
  after the catch, a state-update-after-unmount. Added the guard.
- (codex P1) the line-115 pollingRef bug was already fixed in 453c207 (stale-commit review).
@feruzm

feruzm commented Jun 22, 2026

Copy link
Copy Markdown
Member Author

Thanks for the review — addressed in the latest commits:

Codex P1 (line 115, pollingRef swallows status responses) — already fixed before this review; Codex looked at the stale commit db78ae5a. Current stripe-account-checkout.tsx uses mountedRef and re-checks it both before and after the status await, so success/failed are handled and there's no undefined-throw fall-through to done.

Greptile P1 (redirect-based payment loses post-payment state on return) — real, fixed defensively. Note the currently-enabled methods (card + Google/Apple Pay + 3DS) confirm in-page via confirmPayment(redirect: "if_required"), so a full-page redirect back to returnUrl isn't reachable today; this path only fires if a redirect-only APM (iDEAL/Bancontact/etc.) is later enabled in the dashboard. Because the buyer here is anonymous, a charge-then-blank-form is the worst outcome, so I mirrored the Points resumePaymentIntent convention with one extra step for the missing identity: the checkout carries the (public) username back on returnUrl (?u=), and the premium page reads payment_intent + redirect_status + u on mount and resumes straight into the delivery poll for the already-created order. Critically, resume does not re-mint the PaymentIntent or re-spend the single-use Turnstile token (the mint effect latches startedRef and bails); failed/canceled returns surface an error toast. No email/referral PII is carried (the status endpoint only needs username + payment_intent).

Greptile P2 (tries >= 20 / reschedule tail lacks a mountedRef guard) — fixed; added if (!mountedRef.current) return; after the catch block, before the final-tries setStep/reschedule.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 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 `@apps/web/src/features/shared/purchase-stripe/stripe-account-checkout.tsx`:
- Around line 57-64: The createIntentError function parameter is typed as any
which bypasses TypeScript strict mode. Change the parameter type from any to
unknown, then add proper type narrowing before accessing the response and status
properties. You can use optional chaining with type guards or a conditional
check to safely narrow the unknown type and verify that e is an object with a
response property before accessing e?.response?.status. This ensures type safety
compliance while maintaining the same error handling logic.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 02344184-3eef-4732-a6a7-f4651af6a636

📥 Commits

Reviewing files that changed from the base of the PR and between c66cdfc and 17cf8bf.

📒 Files selected for processing (5)
  • apps/web/src/app/signup/premium/_page.tsx
  • apps/web/src/features/i18n/locales/en-US.json
  • apps/web/src/features/shared/purchase-stripe/index.ts
  • apps/web/src/features/shared/purchase-stripe/stripe-account-checkout.tsx
  • apps/web/src/features/shared/purchase-stripe/use-stripe-account-purchase.ts

Comment on lines +57 to +64
function createIntentError(e: any): string {
const status = e?.response?.status;
// 406 = captcha rejected; 409 = name taken/reserved; 503 = could not verify availability.
if (status === 406) return i18next.t("sign-up.captcha-failed");
if (status === 409) return i18next.t("sign-up.username-exists");
if (status === 503) return i18next.t("sign-up.account-verify-retry");
return i18next.t("sign-up.account-pay-failed");
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify explicit any usage in TS/TSX changed areas
rg -n --type ts --type tsx '\b:\s*any\b' -C2

Repository: ecency/vision-next

Length of output: 90


🏁 Script executed:

cd apps/web && cat -n src/features/shared/purchase-stripe/stripe-account-checkout.tsx | head -70

Repository: ecency/vision-next

Length of output: 3680


🏁 Script executed:

cat tsconfig.json | grep -A5 -B5 '"strict"'

Repository: ecency/vision-next

Length of output: 108


🏁 Script executed:

# Also check for any type usage with a different approach
rg -n 'any' --glob '*.tsx' --glob '*.ts' apps/web/src/features/shared/purchase-stripe/stripe-account-checkout.tsx

Repository: ecency/vision-next

Length of output: 110


🏁 Script executed:

find . -maxdepth 3 -name "tsconfig.json" -type f

Repository: ecency/vision-next

Length of output: 249


🏁 Script executed:

# Check the repo root tsconfig for strict mode
cat tsconfig.json | grep -A10 '"compilerOptions"' | grep '"strict'

Repository: ecency/vision-next

Length of output: 108


🏁 Script executed:

# Check if there are other functions with similar error handling to understand pattern
rg 'function.*Error.*e:\s*(any|unknown)' --max-count 10

Repository: ecency/vision-next

Length of output: 50376


🏁 Script executed:

cat apps/web/tsconfig.json

Repository: ecency/vision-next

Length of output: 747


Replace any with properly typed unknown parameter.

The function parameter e: any bypasses TypeScript strict mode. With strict mode enabled in tsconfig.json, narrow unknown before reading properties:

Suggested patch
-function createIntentError(e: any): string {
-  const status = e?.response?.status;
+function createIntentError(e: unknown): string {
+  const status =
+    typeof e === "object" && e !== null && "response" in e
+      ? (e as { response?: { status?: number } }).response?.status
+      : undefined;
📝 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
function createIntentError(e: any): string {
const status = e?.response?.status;
// 406 = captcha rejected; 409 = name taken/reserved; 503 = could not verify availability.
if (status === 406) return i18next.t("sign-up.captcha-failed");
if (status === 409) return i18next.t("sign-up.username-exists");
if (status === 503) return i18next.t("sign-up.account-verify-retry");
return i18next.t("sign-up.account-pay-failed");
}
function createIntentError(e: unknown): string {
const status =
typeof e === "object" && e !== null && "response" in e
? (e as { response?: { status?: number } }).response?.status
: undefined;
// 406 = captcha rejected; 409 = name taken/reserved; 503 = could not verify availability.
if (status === 406) return i18next.t("sign-up.captcha-failed");
if (status === 409) return i18next.t("sign-up.username-exists");
if (status === 503) return i18next.t("sign-up.account-verify-retry");
return i18next.t("sign-up.account-pay-failed");
}
🤖 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 `@apps/web/src/features/shared/purchase-stripe/stripe-account-checkout.tsx`
around lines 57 - 64, The createIntentError function parameter is typed as any
which bypasses TypeScript strict mode. Change the parameter type from any to
unknown, then add proper type narrowing before accessing the response and status
properties. You can use optional chaining with type guards or a conditional
check to safely narrow the unknown type and verify that e is an object with a
response property before accessing e?.response?.status. This ensures type safety
compliance while maintaining the same error handling logic.

Source: Coding guidelines

vapi (#33) verifies the Cloudflare Turnstile token server-side via verifyTurnstile,
which reads TURNSTILE_SECRET. vapi runs in the 'vision' swarm stack defined in this
repo, so its runtime env is set by this repo's deploy pipeline (same path as
STRIPE_INTERNAL_SECRET):
- docker-compose.yml + docker-compose.production.yml: pass TURNSTILE_SECRET through to
  the vapi service environment.
- master.yml (deploy-EU/US/SG) + staging.yml: add it to env:, the envs: passthrough
  list, and the export line.

Ships dark: vapi's captchaMode defaults to off, so an unset/empty secret is inert
until the operator sets the repo secret and flips the mode at cutover. Not a
NEXT_PUBLIC build-arg, so it never enters the web bundle; server-side in vapi only.
@feruzm

feruzm commented Jun 22, 2026

Copy link
Copy Markdown
Member Author

Bundled the TURNSTILE_SECRET deploy wiring into this PR (commit 4f1e52a19) — vapi (#33) does the server-side verifyTurnstile siteverify, and vapi runs in the vision swarm stack defined in this repo, so its runtime env is configured here (same path as STRIPE_INTERNAL_SECRET):

  • apps/web/docker-compose.production.yml + apps/web/docker-compose.yml → vapi environment: passthrough - TURNSTILE_SECRET
  • .github/workflows/master.yml (deploy-EU/US/SG) + staging.yml → added to env:, the envs: CSV, and the export line

Ships dark — vapi's captchaMode defaults to off, so an unset/empty secret is inert (an unset Compose passthrough resolves to empty; docker stack deploy doesn't fail). It's not a NEXT_PUBLIC build-arg, so it never enters the web bundle — server-side in the vapi container only.

One manual step (founder): add the TURNSTILE_SECRET repo secret in vision-next settings before flipping captchaMode to soft/hard at cutover.

@feruzm feruzm merged commit bcd8f96 into develop Jun 22, 2026
6 checks passed
@feruzm feruzm deleted the feature/stripe-premium-web branch June 22, 2026 17:20
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