feat(settings): new billing page and paywall view#4293
Conversation
|
Important Review skippedAuto incremental reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
📝 WalkthroughWalkthroughThe pull request refactors the subscription and billing UI across two areas. First, 🚥 Pre-merge checks | ✅ 3 | ❌ 1❌ Failed checks (1 inconclusive)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. 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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (2)
js/app/packages/app/component/paywall/PaywallComponent.tsx (1)
93-95: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winUse
PaywallMessageMetadata.titlein the heading.The new metadata model makes
titlerequired, but the UI still shows a hardcoded header. Wiring the title here keeps per-key copy consistent and avoids metadata drift.Suggested diff
- <h2 class="text-2xl text-ink font-semibold"> - Unlock Premium features - </h2> + <h2 class="text-2xl text-ink font-semibold"> + {paywallMetadata()?.title ?? 'Unlock Premium features'} + </h2>🤖 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 `@js/app/packages/app/component/paywall/PaywallComponent.tsx` around lines 93 - 95, The h2 heading in PaywallComponent is using a hardcoded string "Unlock Premium features" instead of leveraging the PaywallMessageMetadata.title property. Replace the hardcoded heading text with the title value from PaywallMessageMetadata to ensure consistency across different paywall configurations and avoid metadata drift. This aligns with the metadata model where title is now required.js/app/packages/app/component/settings/Billing.tsx (1)
73-92: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low valueSurface checkout/portal errors to the user.
Both handlers swallow failures with only
console.error. If Stripe session creation fails, the user clicks and nothing happens with no feedback. Consider a toast/error state so the action doesn't appear broken.🤖 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 `@js/app/packages/app/component/settings/Billing.tsx` around lines 73 - 92, Both the handleCheckout and handleManage functions catch errors from Stripe API calls but only log them to console without providing any user-facing feedback, leaving users uncertain when the action fails. Replace the console.error calls in both catch blocks with user-visible error handling such as a toast notification or error state variable that communicates the failure to the user, ensuring they understand why the checkout or portal session creation did not proceed.
🤖 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 `@js/app/packages/app/component/settings/Billing.tsx`:
- Line 114: In the Billing.tsx file, the header element has a misspelled
Tailwind CSS utility class. Locate the header with class="flex items-cetner
gap-2" and correct the typo by changing items-cetner to items-center. This will
ensure the flex container properly centers its items vertically as intended.
- Around line 137-183: The Premium upgrade card wrapped in the Show component
with `when={!hasPaid()}` is missing authorization checks that are present on the
"Manage" button. To fix this, add authorization conditions to the Show
component's when prop to gate the upgrade card by the same checks used for the
"Manage" button. Specifically, combine the existing `!hasPaid()` condition with
`canManageSubscription()` and the team role check `(!teamRole() || teamRole()
=== 'owner')` using logical AND operators to ensure only authorized users (with
proper permissions and owner role when applicable) can see and trigger the
`handleCheckout` function.
---
Nitpick comments:
In `@js/app/packages/app/component/paywall/PaywallComponent.tsx`:
- Around line 93-95: The h2 heading in PaywallComponent is using a hardcoded
string "Unlock Premium features" instead of leveraging the
PaywallMessageMetadata.title property. Replace the hardcoded heading text with
the title value from PaywallMessageMetadata to ensure consistency across
different paywall configurations and avoid metadata drift. This aligns with the
metadata model where title is now required.
In `@js/app/packages/app/component/settings/Billing.tsx`:
- Around line 73-92: Both the handleCheckout and handleManage functions catch
errors from Stripe API calls but only log them to console without providing any
user-facing feedback, leaving users uncertain when the action fails. Replace the
console.error calls in both catch blocks with user-visible error handling such
as a toast notification or error state variable that communicates the failure to
the user, ensuring they understand why the checkout or portal session creation
did not proceed.
🪄 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: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: da36bdfa-a90e-43cd-8309-72efdf95e03a
📒 Files selected for processing (9)
js/app/packages/app/component/paywall/Paywall.tsxjs/app/packages/app/component/paywall/PaywallComponent.tsxjs/app/packages/app/component/settings/Account.tsxjs/app/packages/app/component/settings/Billing.tsxjs/app/packages/app/component/settings/Settings.tsxjs/app/packages/app/component/settings/Team.tsxjs/app/packages/core/constant/PaywallState.tsxjs/app/packages/core/constant/SettingsState.tsxjs/app/packages/core/constant/settingsTabsConfig.tsx
|
|
||
| <Surface class="flex flex-col rounded-lg p-4" depth={2}> | ||
| <section class="flex flex-col gap-4"> | ||
| <header class="flex items-cetner gap-2"> |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Typo in class name: items-cetner → items-center.
The misspelled utility class is a no-op, so this header won't get the intended centering.
🔤 Proposed fix
- <header class="flex items-cetner gap-2">
+ <header class="flex items-center gap-2">📝 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.
| <header class="flex items-cetner gap-2"> | |
| <header class="flex items-center gap-2"> |
🤖 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 `@js/app/packages/app/component/settings/Billing.tsx` at line 114, In the
Billing.tsx file, the header element has a misspelled Tailwind CSS utility
class. Locate the header with class="flex items-cetner gap-2" and correct the
typo by changing items-cetner to items-center. This will ensure the flex
container properly centers its items vertically as intended.
| <Show | ||
| when={ | ||
| canManageSubscription() && | ||
| hasPaid() && | ||
| (!teamRole() || teamRole() === 'owner') | ||
| } | ||
| > | ||
| <Button | ||
| class="ml-auto rounded-full bg-active" | ||
| size="sm" | ||
| depth={2} | ||
| variant="base" | ||
| onClick={handleManage} | ||
| > | ||
| Manage | ||
| </Button> | ||
| </Show> | ||
| </header> | ||
| <ul class="border-t border-t-edge-muted pt-4 flex flex-wrap gap-4 text-sm text-ink-muted"> | ||
| <PlanFeatures tier={hasPaid() ? 'premium' : 'free'} /> | ||
| </ul> | ||
| </section> | ||
| </Surface> | ||
| <Show when={!hasPaid()}> | ||
| <Surface class="flex flex-col rounded-lg p-4" depth={2}> | ||
| <section class="flex flex-col gap-4"> | ||
| <header class="flex items-center gap-2"> | ||
| <div class="flex flex-col"> | ||
| <h1 class="text-lg font-medium text-ink">Premium</h1> | ||
| <p class="text-ink-extra-muted text-xs">$40 per seat / month</p> | ||
| </div> | ||
|
|
||
| <Button | ||
| class="ml-auto rounded-full py-1.5 px-3" | ||
| depth={2} | ||
| variant="cta" | ||
| onClick={handleCheckout} | ||
| > | ||
| Upgrade now | ||
| </Button> | ||
| </header> | ||
| <ul class="border-t border-t-edge-muted pt-4 flex flex-wrap gap-4 text-sm text-ink-muted"> | ||
| <PlanFeatures tier="premium" /> | ||
| </ul> | ||
| </section> | ||
| </Surface> | ||
| </Show> |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟡 Minor
🧩 Analysis chain
🏁 Script executed:
find . -type f -name "Billing.tsx" | head -5Repository: macro-inc/macro
Length of output: 206
🏁 Script executed:
# Search for canManageSubscription definition
rg "canManageSubscription" -t ts -t tsx --max-count=20Repository: macro-inc/macro
Length of output: 181
🏁 Script executed:
# Look for handleCheckout implementation
rg "handleCheckout" -t ts -t tsx -B 2 -A 10Repository: macro-inc/macro
Length of output: 181
🏁 Script executed:
rg "canManageSubscription" --max-count=20Repository: macro-inc/macro
Length of output: 153
🏁 Script executed:
rg "handleCheckout" -B 2 -A 10Repository: macro-inc/macro
Length of output: 153
🏁 Script executed:
# Read the Billing.tsx file to understand the full context
cat -n ./js/app/packages/app/component/settings/Billing.tsx | head -200Repository: macro-inc/macro
Length of output: 7240
🏁 Script executed:
# Look for stripeServiceClient implementation
find . -type f -name "*stripe*client*" -o -name "*stripe*service*"Repository: macro-inc/macro
Length of output: 222
🏁 Script executed:
# Search for createCheckoutSessionV2 implementation
rg "createCheckoutSessionV2" -B 3 -A 15Repository: macro-inc/macro
Length of output: 153
🏁 Script executed:
# Look for Stripe endpoints/handlers on the backend
find . -type f -name "*.rs" | xargs grep -l "checkout" 2>/dev/null | head -5Repository: macro-inc/macro
Length of output: 492
🏁 Script executed:
# Search for WRITE_STRIPE_SUBSCRIPTION permission usage
rg "WRITE_STRIPE_SUBSCRIPTION" -B 2 -A 5Repository: macro-inc/macro
Length of output: 153
🏁 Script executed:
# Read the checkout session endpoint
cat -n ./rust/cloud-storage/authentication_service/src/api/user/stripe/create_checkout_session_v2.rsRepository: macro-inc/macro
Length of output: 7949
🏁 Script executed:
# Check the stripe module for authorization
cat -n ./rust/cloud-storage/authentication_service/src/api/user/stripe/mod.rsRepository: macro-inc/macro
Length of output: 331
🏁 Script executed:
# Search for permission checks on stripe endpoints
rg "WRITE_STRIPE_SUBSCRIPTION\|checkout\|stripe" ./rust/cloud-storage/authentication_service/src/api/user/stripe/ -B 2 -A 3Repository: macro-inc/macro
Length of output: 153
🏁 Script executed:
# Look for team/role checks in authentication service
rg "team.*owner\|owner.*role" ./rust/cloud-storage/authentication_service/ -B 1 -A 2 | head -50Repository: macro-inc/macro
Length of output: 153
🏁 Script executed:
# Check the entity_access extractor to understand OwnerTeamRole behavior
rg "OptionalMacroUserTeamExtractor\|OwnerTeamRole" ./rust/cloud-storage/entity_access/ -B 2 -A 5 | head -60Repository: macro-inc/macro
Length of output: 153
🏁 Script executed:
# Look for how the checkout endpoint is actually secured/restricted
cat -n ./rust/cloud-storage/authentication_service/src/api/user/mod.rs | grep -A 5 -B 5 "checkout"Repository: macro-inc/macro
Length of output: 639
🏁 Script executed:
# Check if there's any permission enforcement middleware or guards
rg "WRITE_STRIPE\|permission.*check\|authorize" ./rust/cloud-storage/authentication_service/src/ -B 1 -A 3 | head -80Repository: macro-inc/macro
Length of output: 153
🏁 Script executed:
# Check the OptionalMacroUserTeamExtractor implementation
find ./rust/cloud-storage -name "*.rs" -type f | xargs grep -l "OptionalMacroUserTeamExtractor" | head -3Repository: macro-inc/macro
Length of output: 428
🏁 Script executed:
# Look at the user permissions middleware
cat -n ./rust/cloud-storage/authentication_service/src/middleware/user_permissions/attach_user_permissions.rs | head -100Repository: macro-inc/macro
Length of output: 288
🏁 Script executed:
# Search for permission enforcement on stripe endpoints specifically
rg "stripe\|checkout" ./rust/cloud-storage/authentication_service/src/middleware/ -B 3 -A 5Repository: macro-inc/macro
Length of output: 253
🏁 Script executed:
# Check if there are guards or extractors that enforce WRITE_STRIPE_SUBSCRIPTION
rg "OwnerTeamRole\|from_request\|FromRequest" ./rust/cloud-storage/entity_access/ -B 1 -A 8 | head -80Repository: macro-inc/macro
Length of output: 153
🏁 Script executed:
# Find entity_access module structure
find ./rust/cloud-storage -type d -name "entity_access"Repository: macro-inc/macro
Length of output: 188
🏁 Script executed:
# Search for OptionalMacroUserTeamExtractor definition
find ./rust/cloud-storage -name "*.rs" -type f -exec grep -l "struct OptionalMacroUserTeamExtractor\|impl.*OptionalMacroUserTeamExtractor" {} \;Repository: macro-inc/macro
Length of output: 224
🏁 Script executed:
# Look for axum extractors in entity_access
find ./rust/cloud-storage -path "*entity_access*" -name "*.rs" | xargs grep -l "axum_extractor\|FromRequest" 2>/dev/nullRepository: macro-inc/macro
Length of output: 1021
🏁 Script executed:
# Check what OwnerTeamRole actually validates
rg "OwnerTeamRole" ./rust/cloud-storage -B 3 -A 10 | head -100Repository: macro-inc/macro
Length of output: 8786
🏁 Script executed:
# Read the team extractor implementation
cat -n ./rust/cloud-storage/entity_access/src/inbound/axum_extractors/team.rsRepository: macro-inc/macro
Length of output: 6342
🏁 Script executed:
# Look for how OptionalMacroUserTeamExtractor works differently
rg "Optional.*Extractor|impl.*Optional" ./rust/cloud-storage/entity_access/src/inbound/axum_extractors/ -B 3 -A 10Repository: macro-inc/macro
Length of output: 32907
🏁 Script executed:
# Check if there are any permission enforcement guards on the stripe routes
rg "WRITE_STRIPE\|stripe\|checkout" ./rust/cloud-storage/authentication_service/src/api/user/mod.rs -B 5 -A 5Repository: macro-inc/macro
Length of output: 153
🏁 Script executed:
# Look for how permissions are attached/enforced on routes
find ./rust/cloud-storage -name "*macro_middleware*" -o -name "*permission*middleware*" | head -5Repository: macro-inc/macro
Length of output: 191
🏁 Script executed:
# Check whether the permissions extractor/middleware enforces anything
rg "attach_user_permissions\|user_permissions" ./rust/cloud-storage -l | head -5Repository: macro-inc/macro
Length of output: 153
🏁 Script executed:
# Look for macro_middleware structure and user_permissions
find ./rust/cloud-storage/macro_middleware -type f -name "*.rs" | head -10Repository: macro-inc/macro
Length of output: 853
🏁 Script executed:
# Check the user/mod.rs more carefully for the full routing and any guard logic
cat -n ./rust/cloud-storage/authentication_service/src/api/user/mod.rs | head -100Repository: macro-inc/macro
Length of output: 4080
🏁 Script executed:
# Look for any subscription-related business logic or comments
rg "subscription\|checkout\|upgrade" ./rust/cloud-storage/authentication_service/src/ -B 2 -A 2 | head -100Repository: macro-inc/macro
Length of output: 153
Add authorization checks to the Premium upgrade card for consistency with the "Manage" button.
The checkout endpoint (/stripe/checkoutv2) does not enforce WRITE_STRIPE_SUBSCRIPTION permission or owner-only access server-side. Any authenticated team member can invoke handleCheckout(), even though the "Manage" button is gated by both permission and role checks. Gate the upgrade card by the same conditions to prevent unauthorized members from initiating checkout:
- <Show when={!hasPaid()}>
+ <Show when={!hasPaid() && canManageSubscription() && (!teamRole() || teamRole() === 'owner')}>📝 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.
| <Show | |
| when={ | |
| canManageSubscription() && | |
| hasPaid() && | |
| (!teamRole() || teamRole() === 'owner') | |
| } | |
| > | |
| <Button | |
| class="ml-auto rounded-full bg-active" | |
| size="sm" | |
| depth={2} | |
| variant="base" | |
| onClick={handleManage} | |
| > | |
| Manage | |
| </Button> | |
| </Show> | |
| </header> | |
| <ul class="border-t border-t-edge-muted pt-4 flex flex-wrap gap-4 text-sm text-ink-muted"> | |
| <PlanFeatures tier={hasPaid() ? 'premium' : 'free'} /> | |
| </ul> | |
| </section> | |
| </Surface> | |
| <Show when={!hasPaid()}> | |
| <Surface class="flex flex-col rounded-lg p-4" depth={2}> | |
| <section class="flex flex-col gap-4"> | |
| <header class="flex items-center gap-2"> | |
| <div class="flex flex-col"> | |
| <h1 class="text-lg font-medium text-ink">Premium</h1> | |
| <p class="text-ink-extra-muted text-xs">$40 per seat / month</p> | |
| </div> | |
| <Button | |
| class="ml-auto rounded-full py-1.5 px-3" | |
| depth={2} | |
| variant="cta" | |
| onClick={handleCheckout} | |
| > | |
| Upgrade now | |
| </Button> | |
| </header> | |
| <ul class="border-t border-t-edge-muted pt-4 flex flex-wrap gap-4 text-sm text-ink-muted"> | |
| <PlanFeatures tier="premium" /> | |
| </ul> | |
| </section> | |
| </Surface> | |
| </Show> | |
| <Show | |
| when={ | |
| canManageSubscription() && | |
| hasPaid() && | |
| (!teamRole() || teamRole() === 'owner') | |
| } | |
| > | |
| <Button | |
| class="ml-auto rounded-full bg-active" | |
| size="sm" | |
| depth={2} | |
| variant="base" | |
| onClick={handleManage} | |
| > | |
| Manage | |
| </Button> | |
| </Show> | |
| </header> | |
| <ul class="border-t border-t-edge-muted pt-4 flex flex-wrap gap-4 text-sm text-ink-muted"> | |
| <PlanFeatures tier={hasPaid() ? 'premium' : 'free'} /> | |
| </ul> | |
| </section> | |
| </Surface> | |
| <Show when={!hasPaid() && canManageSubscription() && (!teamRole() || teamRole() === 'owner')}> | |
| <Surface class="flex flex-col rounded-lg p-4" depth={2}> | |
| <section class="flex flex-col gap-4"> | |
| <header class="flex items-center gap-2"> | |
| <div class="flex flex-col"> | |
| <h1 class="text-lg font-medium text-ink">Premium</h1> | |
| <p class="text-ink-extra-muted text-xs">$40 per seat / month</p> | |
| </div> | |
| <Button | |
| class="ml-auto rounded-full py-1.5 px-3" | |
| depth={2} | |
| variant="cta" | |
| onClick={handleCheckout} | |
| > | |
| Upgrade now | |
| </Button> | |
| </header> | |
| <ul class="border-t border-t-edge-muted pt-4 flex flex-wrap gap-4 text-sm text-ink-muted"> | |
| <PlanFeatures tier="premium" /> | |
| </ul> | |
| </section> | |
| </Surface> | |
| </Show> |
🤖 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 `@js/app/packages/app/component/settings/Billing.tsx` around lines 137 - 183,
The Premium upgrade card wrapped in the Show component with `when={!hasPaid()}`
is missing authorization checks that are present on the "Manage" button. To fix
this, add authorization conditions to the Show component's when prop to gate the
upgrade card by the same checks used for the "Manage" button. Specifically,
combine the existing `!hasPaid()` condition with `canManageSubscription()` and
the team role check `(!teamRole() || teamRole() === 'owner')` using logical AND
operators to ensure only authorized users (with proper permissions and owner
role when applicable) can see and trigger the `handleCheckout` function.
Uh oh!
There was an error while loading. Please reload this page.