Skip to content

fix(frontend): plan page dark-mode loader and checkout popup - #2900

Open
riderx wants to merge 2 commits into
mainfrom
cursor/29ce5f5c
Open

fix(frontend): plan page dark-mode loader and checkout popup#2900
riderx wants to merge 2 commits into
mainfrom
cursor/29ce5f5c

Conversation

@riderx

@riderx riderx commented Aug 6, 2026

Copy link
Copy Markdown
Member

Summary (AI generated)

  • Plan subscribe spinner now inherits button text color so it stays visible on dark-mode white buttons
  • Web checkout always uses a confirm dialog with a real _blank link after Stripe session creation, avoiding popup blockers
  • DialogV2 lets target="_blank" anchors open natively instead of preventDefault + window.open
  • openBlank falls back to a confirm link when the browser blocks the popup

Motivation (AI generated)

Dark mode made the subscribe loading spinner invisible (text-white on dark:bg-white). Checkout also opened with window.open after an async API call, which browsers often block.

Business Impact (AI generated)

Users can see checkout loading in dark mode and complete plan upgrades without a blocked Stripe tab.

Test Plan (AI generated)

  • Open /settings/organization/plans in dark mode and click a plan CTA — spinner is visible on black and white buttons
  • Confirm checkout dialog appears on web and opens Stripe in a new tab
  • Cancel the confirm dialog — loading stops and no tab opens
  • If a browser blocks window.open, confirm the fallback dialog can open the URL

Generated with AI

Made with Cursor


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

Review in cubic

Summary by CodeRabbit

  • New Features

    • Web checkout now opens in a new browser tab.
    • Added a fallback link when the browser blocks the new tab.
    • Added localized messaging explaining how to open checkout manually.
  • Bug Fixes

    • Improved checkout handling across web, mobile, and iOS platforms.
    • Stripe portal links now reliably open or provide a manual fallback.
    • Updated checkout confirmation messaging and tracking.

Inherit spinner color for dark-mode plan buttons and open Stripe checkout via a confirm link so browsers do not block the tab after async session creation.

Co-authored-by: Cursor <cursoragent@cursor.com>
@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Web Stripe checkout now uses a confirmation flow across web platforms. Blocked popups show a manual checkout link. Dialog handling and E2E coverage were updated for the new-tab flow.

Changes

Stripe checkout flow

Layer / File(s) Summary
Web checkout routing
src/pages/settings/organization/Plans.vue, messages/en.json
Web platforms use the generic checkout confirmation flow. Safari-specific detection and tracking were removed. New localization strings describe new-tab checkout and blocked-popup recovery.
Blocked popup fallback
src/services/stripe.ts, src/components/DialogV2.vue
openBlank now returns an asynchronous result and shows a manual external link when window.open fails. _blank links close the dialog without blocking navigation. Stripe portal confirmation awaits the result.
Checkout flow validation
playwright/e2e/subscription-checkout.spec.ts
The E2E test validates the visible confirmation link and Stripe emulator checkout URL before navigation.

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

Sequence Diagram(s)

sequenceDiagram
  participant Plans
  participant openBlank
  participant Browser
  participant DialogV2
  Plans->>openBlank: Open Stripe checkout URL
  openBlank->>Browser: Call window.open
  Browser-->>openBlank: Return popup result
  openBlank->>DialogV2: Show manual link when blocked
  DialogV2->>Browser: Open checkout link in a new tab
Loading

Possibly related PRs

Suggested labels: codex

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the dark-mode loader fix and checkout popup changes, which match the primary changes in the pull request.
Description check ✅ Passed The description includes a clear summary and test plan, but it omits the template's Screenshots and Checklist sections.
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 docstrings
  • Create stacked PR
  • Commit on current branch

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

@cursor

cursor Bot commented Aug 6, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_7fb8b852-c486-4fbd-9945-848987c09f8d)

@cubic-dev-ai cubic-dev-ai 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.

All reported issues were addressed across 4 files

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread src/services/stripe.ts Outdated
Comment thread src/pages/settings/organization/Plans.vue
@codspeed-hq

codspeed-hq Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 43 untouched benchmarks
⏩ 2 skipped benchmarks1


Comparing cursor/29ce5f5c (2a2d877) with main (0fdf275)2

Open in CodSpeed

Footnotes

  1. 2 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

  2. No successful run was found on main (ec1c4f1) during the generation of this report, so 0fdf275 was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

Propagate iOS action-sheet cancel from openBlank and align the subscription checkout E2E with the confirm-link web flow.

Co-authored-by: Cursor <cursoragent@cursor.com>
@cursor

cursor Bot commented Aug 6, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_aae79b41-24a0-411a-8712-24009d1719b1)

@coderabbitai coderabbitai Bot added the codex label Aug 6, 2026

@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 `@src/services/stripe.ts`:
- Around line 62-64: Update the popup-opening flow around the opened result from
globalThis.open to set opened.opener to null before returning true; preserve the
existing null-return fallback unchanged.
🪄 Autofix

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: ASSERTIVE

Plan: Pro

Run ID: 0fe8e1e1-4d43-4f9d-9e1b-c246893fda06

📥 Commits

Reviewing files that changed from the base of the PR and between 0fdf275 and 2a2d877.

📒 Files selected for processing (5)
  • messages/en.json
  • playwright/e2e/subscription-checkout.spec.ts
  • src/components/DialogV2.vue
  • src/pages/settings/organization/Plans.vue
  • src/services/stripe.ts
🔗 Linked repositories identified

CodeRabbit considers these linked repositories for cross-repo context during reviews:

  • Cap-go/capacitor-updater (manual)

Comment thread src/services/stripe.ts
Comment on lines +62 to +64
const opened = globalThis.open(link, '_blank')
if (opened)
return true

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.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- src/services/stripe.ts ---'
cat -n src/services/stripe.ts
printf '%s\n' '--- window.open usages ---'
rg -n -C 3 'globalThis\.open|window\.open|noopener|opener' src tests .github 2>/dev/null || true
printf '%s\n' '--- relevant package/runtime metadata ---'
rg -n -C 2 '"(lint|test|typecheck|build)"|vue|vite|vitest|playwright' package.json bun.lockb package-lock.json yarn.lock pnpm-lock.yaml 2>/dev/null || true

Repository: Cap-go/capgo.app

Length of output: 36115


🌐 Web query:

MDN Window open target _blank implicit noopener window.opener browser compatibility

💡 Result:

In modern web browsers, setting target="_blank" on , , or

elements implicitly behaves as if rel="noopener" has been specified [1]. This means the opened browsing context will not have access to the opening window via the window.opener property, which will return null [2][1]. This implicit behavior was standardized to improve security by preventing the opened page from tampering with the original page via the window.opener reference [1][3]. If you explicitly require window.opener access, you must now use rel="opener" on the link [1][3]. Browser Compatibility: This implicit behavior is widely supported in modern browsers [4]. Support for target="_blank" implying rel="noopener" generally aligns with the adoption of this standard in major browser engines (Chromium 88+, Firefox 79+, and Safari 12.1+) [4]. Older browsers do not provide this implicit security, which is why developers previously had to manually add rel="noopener" to secure links [3][5]. Note: This implicit behavior applies to HTML anchor/form elements [1]. When using the window.open JavaScript method, the noopener behavior is not automatic and must still be explicitly requested by including "noopener" in the windowFeatures parameter [6][7].

Citations:


🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- browser-target and compatibility configuration ---'
rg -n -i -C 2 'browserslist|browser|target|support|compatib' package.json vite.config.* .browserslistrc tsconfig*.json README.md docs 2>/dev/null || true
printf '%s\n' '--- tests and history for openBlank ---'
rg -n -C 5 'openBlank|presentBlockedPopupFallback|window\.opener' tests src scripts 2>/dev/null || true
git diff --stat
git log --oneline -8 -- src/services/stripe.ts

Repository: Cap-go/capgo.app

Length of output: 50375


🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- browser support configuration ---'
for f in package.json vite.config.ts vite.config.js vite.config.mts .browserslistrc; do
  if [ -f "$f" ]; then
    printf '\n[%s]\n' "$f"
    rg -n -i 'browserslist|target|browser|support|compatib' "$f" || true
  fi
done
printf '%s\n' '--- focused openBlank history ---'
git log --oneline -12 -- src/services/stripe.ts
printf '%s\n' '--- focused tests/usages ---'
rg -n -C 4 'openBlank|presentActionSheetOpen|window\.opener' tests src/services src/pages/app/'[app]'.channel.'[channel]'.vue 2>/dev/null || true
printf '%s\n' '--- current diff summary ---'
git diff --stat

Repository: Cap-go/capgo.app

Length of output: 3274


🏁 Script executed (no clone):

node - <<'JS'
'use strict'

// Read-only behavioral model of the proposed operation.
// It checks that the popup-blocked branch still depends on the return value
// and that a successfully returned WindowProxy gets opener cleared.
function openBlankModel(open) {
  const opened = open('https://checkout.example', '_blank')
  if (opened) {
    opened.opener = null
    return true
  }
  return 'fallback'
}

const popup = { opener: { location: 'https://app.example' } }
console.log('opened:', openBlankModel(() => popup))
console.log('opener:', popup.opener)
console.log('blocked:', openBlankModel(() => null))
JS

Length of output: 169


🌐 Web query:

MDN Window.open noopener feature return value WindowProxy opener null

💡 Result:

When the noopener feature is used in the window.open() method, the method returns null instead of a WindowProxy object [1][2]. Additionally, the newly opened window will have its window.opener property set to null [3][4]. This behavior ensures that the new browsing context has no access to the originating window, which is a key security feature to prevent unauthorized access or tampering [4][2]. This behavior is consistent with the HTML specification, which dictates that window.open() returns null when the noopener option is provided, effectively preventing the caller from obtaining a reference to the opened window [5][6][7].

Citations:


Clear opened.opener before returning success. Set opened.opener = null when the popup opens. Keep the null-return fallback intact.

🤖 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 `@src/services/stripe.ts` around lines 62 - 64, Update the popup-opening flow
around the opened result from globalThis.open to set opened.opener to null
before returning true; preserve the existing null-return fallback unchanged.

@sonarqubecloud

sonarqubecloud Bot commented Aug 6, 2026

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai 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.

1 issue found across 2 files (changes from recent commits).

Confidence score: 4/5

  • In playwright/e2e/subscription-checkout.spec.ts, the test uses page.goto on the Confirm link href instead of exercising the real target="_blank" click flow, so the new popup-opening path (and popup-blocker fallback) can regress without being caught in CI; update the spec to click the link and assert popup/new-page behavior directly.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="playwright/e2e/subscription-checkout.spec.ts">

<violation number="1" location="playwright/e2e/subscription-checkout.spec.ts:205">
P2: Reading the Confirm link's href and navigating with page.goto bypasses the actual target=_blank popup this PR is meant to harden, so the new `_blank` open path and popup-blocker fallback are never e2e-covered. Consider clicking the link and asserting the resulting popup (e.g. `const [popup] = await Promise.all([page.waitForEvent('popup'), confirmLink.click()])`, then assert `popup.url()` matches the Stripe origin) so a regression in DialogV2's native-blank handling is caught.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

expect(checkoutUrl!).toMatch(new RegExp(`${escapedStripeOrigin}/checkout/`))

await page.goto(checkoutUrl)
await page.goto(checkoutUrl!)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: Reading the Confirm link's href and navigating with page.goto bypasses the actual target=_blank popup this PR is meant to harden, so the new _blank open path and popup-blocker fallback are never e2e-covered. Consider clicking the link and asserting the resulting popup (e.g. const [popup] = await Promise.all([page.waitForEvent('popup'), confirmLink.click()]), then assert popup.url() matches the Stripe origin) so a regression in DialogV2's native-blank handling is caught.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At playwright/e2e/subscription-checkout.spec.ts, line 205:

<comment>Reading the Confirm link's href and navigating with page.goto bypasses the actual target=_blank popup this PR is meant to harden, so the new `_blank` open path and popup-blocker fallback are never e2e-covered. Consider clicking the link and asserting the resulting popup (e.g. `const [popup] = await Promise.all([page.waitForEvent('popup'), confirmLink.click()])`, then assert `popup.url()` matches the Stripe origin) so a regression in DialogV2's native-blank handling is caught.</comment>

<file context>
@@ -206,14 +193,16 @@ test.describe('Subscription Checkout', () => {
+    expect(checkoutUrl!).toMatch(new RegExp(`${escapedStripeOrigin}/checkout/`))
 
-    await page.goto(checkoutUrl)
+    await page.goto(checkoutUrl!)
     await expect(page).toHaveURL(new RegExp(`${escapedStripeOrigin}/checkout/`))
     await page.getByRole('button', { name: /^Pay / }).click()
</file context>

@riderx
riderx deployed to deepsec-pr August 6, 2026 20:56 — with GitHub Actions Active
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant