Conversation
## Walkthrough
This update introduces comprehensive Stripe billing integration, replacing and supplementing Lemon Squeezy for premium subscription management. It updates environment variables, database schema, premium tier logic, and payment-related actions. The codebase now handles Stripe webhooks, checkout, and billing portal flows, adds new premium tiers, and removes legacy feature access flags and Lemon Squeezy payment link dependencies.
## Changes
| File(s) / Path(s) | Change Summary |
|----------------------------------------------------------------------------------------------------------------|---------------|
| `apps/web/.env.example`, `turbo.json`, `apps/web/env.ts` | Removed Lemon Squeezy payment link env variables; added Stripe secret keys and price IDs. |
| `apps/web/prisma/schema.prisma`, `apps/web/prisma/migrations/20250506025728_stripe/migration.sql` | Added Stripe-related fields and new premium tiers to schema; removed feature access columns. |
| `apps/web/package.json` | Added Stripe dependencies. |
| `apps/web/ee/LICENSE.md` | Added new commercial license file. |
| `apps/web/ee/billing/stripe/index.ts`, `apps/web/ee/billing/stripe/sync-stripe.ts` | Added Stripe integration utilities for client initialization and subscription syncing. |
| `apps/web/ee/billing/lemon/index.ts` | Commented out Lemon Squeezy plan switching logic. |
| `apps/web/app/api/stripe/generate-checkout/route.ts`, `apps/web/app/api/stripe/success/route.ts`, `apps/web/app/api/stripe/webhook/route.ts` | Added new API routes for Stripe checkout, success, and webhook handling. |
| `apps/web/app/api/lemon-squeezy/webhook/route.ts` | Refactored Lemon Squeezy webhook: removed lifetime logic, improved event tracking, aligned with new premium tier logic. |
| `apps/web/app/api/user/me/route.ts`, `apps/web/app/api/google/watch/all/route.ts`, `apps/web/app/api/google/webhook/process-history.ts` | Updated queries to use new premium tier and Stripe fields; removed feature access checks. |
| `apps/web/app/api/google/webhook/process-history-item.ts`, `apps/web/app/api/google/webhook/process-history-item.test.ts`, `apps/web/app/api/google/webhook/types.ts` | Unified AI access checks into a single flag; updated types and logic accordingly. |
| `apps/web/app/(app)/premium/config.ts`, `apps/web/app/(app)/premium/Pricing.tsx` | Refactored premium tier definitions and pricing UI; added new "Business Plus" tier, removed "basic" and "enterprise" tiers. |
| `apps/web/app/(app)/admin/AdminUpgradeUserForm.tsx`, `apps/web/app/(app)/admin/validation.tsx` | Updated admin UI and validation for new premium tiers; removed "lifetime" tier. |
| `apps/web/app/(app)/[emailAccountId]/automation/knowledge/KnowledgeForm.tsx`, `apps/web/app/(app)/[emailAccountId]/automation/knowledge/KnowledgeBase.tsx`, `apps/web/utils/actions/knowledge.ts`, `apps/web/utils/config.ts` | Enforced knowledge base limits for basic tiers; added premium upgrade prompts and config constants. |
| `apps/web/app/(app)/[emailAccountId]/settings/MultiAccountSection.tsx` | Removed purchase button for extra seats; updated UI to reflect new payment flows. |
| `apps/web/app/(app)/[emailAccountId]/usage/usage.tsx` | Updated premium check to use Stripe subscription status. |
| `apps/web/components/PremiumAlert.tsx`, `apps/web/utils/premium/index.ts`, `apps/web/utils/premium/check-and-redirect-for-upgrade.tsx` | Refactored premium access logic to use tier and Stripe status; removed feature access flags. |
| `apps/web/utils/actions/premium.ts` | Added Stripe checkout and billing portal actions; refactored admin actions; removed Lemon plan switch. |
| `apps/web/utils/premium/server.ts` | Added unified access check; updated seat management for Stripe; renamed Lemon Squeezy functions. |
| `apps/web/utils/premium/create-premium.ts` | Added helper to create premium record for user. |
| `apps/web/utils/posthog.ts`, `apps/web/utils/error.server.ts`, `apps/web/app/api/user/complete-registration/route.ts` | Standardized and expanded PostHog event tracking functions. |
| `apps/web/utils/user/validate.ts` | Changed AI access check to use premium tier. |
| `apps/web/scripts/addLemonDataToPosthog.ts`, `apps/web/scripts/encrypt-tokens.ts` | Deleted legacy scripts for Lemon Squeezy analytics and token encryption. |
| `apps/web/utils/middleware.ts` | Improved error propagation for redirects in middleware. |
## Sequence Diagram(s)
```mermaid
sequenceDiagram
participant User
participant WebApp
participant Stripe
participant DB
User->>WebApp: Click "Upgrade" or "Checkout"
WebApp->>Stripe: Create/Retrieve Stripe customer
Stripe-->>WebApp: Stripe customer object
WebApp->>Stripe: Create checkout session
Stripe-->>WebApp: Checkout session URL
WebApp-->>User: Redirect to Stripe checkout
User->>Stripe: Complete payment
Stripe->>WebApp: Send webhook event
WebApp->>DB: Sync Stripe subscription data
WebApp->>PostHog: Track event
WebApp-->>Stripe: 200 OK
User->>WebApp: Visit /api/stripe/success
WebApp->>DB: Sync Stripe data
WebApp->>PostHog: Track checkout completed
WebApp-->>User: Redirect to setup pagePossibly related PRs
Poem
|
|
The latest updates on your projects. Learn more about Vercel for Git ↗︎
|
There was a problem hiding this comment.
Actionable comments posted: 20
🔭 Outside diff range comments (6)
apps/web/app/api/user/me/route.ts (1)
12-31: 💡 Verification agent🧩 Analysis chain
Verify that all premium-related fields are properly handled
The AI summary mentions that several fields (
bulkUnsubscribeAccess,aiAutomationAccess, andcoldEmailBlockerAccess) were removed from this selection, but I don't see them in the current code. Make sure all premium tier checks now properly use the Stripe subscription status where needed.
🏁 Script executed:
#!/bin/bash # Check for any remaining references to the removed premium access fields echo "Checking for references to removed fields..." rg -A 3 -B 3 "bulkUnsubscribeAccess|aiAutomationAccess|coldEmailBlockerAccess" --type tsLength of output: 719
Cleanup stale premium access fields and enforce Stripe-based checks
I found that the deprecated fields are still referenced in the premium utility, so we need to remove them and ensure all tier logic relies on Stripe subscription status:
• File apps/web/utils/premium/server.ts (around the object literal):
bulkUnsubscribeAccess: null, aiAutomationAccess: null, coldEmailBlockerAccess: null,• Remove these entries and any related code paths.
• Verify that any feature-gating for premium tiers now exclusively usesstripeSubscriptionStatus(or equivalent) rather than legacy flags.apps/web/app/api/google/webhook/process-history-item.test.ts (1)
244-278: 💡 Verification agent🧩 Analysis chain
Function name mismatch with parameter.
The
shouldRunColdEmailBlockerfunction still accepts a parameter namedhasColdEmailAccesswhile the rest of the codebase has been updated to usehasAiAccess.-describe("shouldRunColdEmailBlocker", () => { +describe("shouldRunColdEmailBlocker", () => { it("should return true when coldEmailBlocker is ARCHIVE_AND_READ_AND_LABEL and hasColdEmailAccess is true", () => { const result = shouldRunColdEmailBlocker( ColdEmailSetting.ARCHIVE_AND_READ_AND_LABEL, - true, + true, ); expect(result).toBe(true); });Consider updating the function signature and documentation to use
hasAiAccessparameter name for consistency with the rest of the codebase.
🏁 Script executed:
#!/bin/bash # Check if shouldRunColdEmailBlocker implementation has been updated rg -A 2 "function shouldRunColdEmailBlocker" --glob "*.ts"Length of output: 340
Update test to use
hasAiAccessinstead ofhasColdEmailAccessThe implementation of
shouldRunColdEmailBlockernow expects ahasAiAccessboolean parameter, but the tests still refer to and name ithasColdEmailAccess. Please rename all occurrences in the test suite to keep descriptions and variable names consistent.• File:
apps/web/app/api/google/webhook/process-history-item.test.ts
Lines: 244–278
• Update each test’s description and the parameter name in theshouldRunColdEmailBlockercalls:
- Change “hasColdEmailAccess” → “hasAiAccess” in the
ittitles- Rename the variable passed (and any inline comments) from
hasColdEmailAccesstohasAiAccessExample diff:
- it("should return true when coldEmailBlocker is LABEL and hasColdEmailAccess is true", () => { - const result = shouldRunColdEmailBlocker(ColdEmailSetting.LABEL, true); + it("should return true when coldEmailBlocker is LABEL and hasAiAccess is true", () => { + const result = shouldRunColdEmailBlocker(ColdEmailSetting.LABEL, true);turbo.json (1)
1-116: 💡 Verification agent🧩 Analysis chain
Verify Stripe and Lemon Squeezy coexistence strategy.
The changes indicate a transition from Lemon Squeezy to Stripe while maintaining backward compatibility. Consider documenting the migration strategy and timeline for completely moving away from Lemon Squeezy, if that's the plan.
🌐 Web query:
What are best practices for migrating payment processors while maintaining existing subscriptions?💡 Result:
Migrating payment processors while maintaining existing subscriptions can be complex, but following best practices can help ensure a smooth transition with minimal disruption for customers. Here are some key best practices to consider:
Plan Thoroughly:
- Map out the entire migration process, identifying all stakeholders, timelines, and potential risks.
- Understand the data structure and capabilities of both the current and new payment processors.
Communicate Clearly:
- Inform your customers about the upcoming change well in advance.
- Explain any actions they might need to take, but strive to minimize customer involvement.
Data Migration and Security:
- Ensure secure handling of sensitive payment data during transfer.
- Use tokenization where possible to avoid handling raw card data.
- Verify compliance with PCI DSS requirements throughout the migration.
Maintain Subscription Integrity:
- Migrate all subscription data accurately, including billing cycles, amounts, discounts, and customer preferences.
- If possible, use the new processor’s tools or APIs for importing subscription data.
- Avoid creating duplicate subscriptions or losing billing history.
Test Extensively:
- Run tests on the new processor setup with a subset of accounts to validate billing, renewals, cancellations, etc.
- Ensure error handling and customer notifications work as intended.
- Perform end-to-end testing covering all subscription scenarios.
Parallel Processing:
- Consider running both old and new processors in parallel for a short period.
- This helps catch any issues before fully switching.
Fallback and Rollback Plan:
- Prepare a rollback plan in case the migration encounters critical problems.
- Ensure data consistency and availability for customers during rollback.
Update Internal Systems:
- Synchronize internal billing, CRM, and accounting systems with the new processor data.
- Update customer support scripts and documentation to reflect the new payment processor.
Monitor and Support:
- Monitor all transactions closely immediately after migration.
- Provide dedicated customer support to handle any subscription or payment issues quickly.
Compliance and Legal Review:
- Review contracts and terms of service for changes or notifications required.
- Ensure compliance with data protection regulations like GDPR.
By following these practices, you can minimize risks and provide a seamless experience for your customers during payment processor migration while preserving their existing subscriptions. If you need detailed guidance tailored to your specific processors or platform, I can help with that too.
Document payment processor migration strategy
Please add a clear migration plan and timeline for deprecating Lemon Squeezy and switching fully to Stripe. Right now your turbo.json still lists both sets of env vars:
- LEMONSQUEEZY_SIGNING_SECRET
- LEMONSQUEEZY_API_KEY
- …
- STRIPE_SECRET_KEY
- STRIPE_WEBHOOK_SECRET
- etc.
Based on industry best practices for preserving existing subscriptions, you should:
Create or update a dedicated migration guide (e.g. docs/migrations/payment-processor.md) that covers:
• Stakeholders, timelines, and notification plan for customers
• Secure data migration steps (tokenization, PCI-DSS compliance)
• Parallel-processing period and rollback strategy
• End-to-end testing scenarios (renewals, cancellations, discounts)Update internal docs/README to reflect both providers during transition and the final cutover date when Lemon Squeezy vars will be removed.
Ensure your CI/CD (turbo.json) env list stays in sync: after cutover, remove the
LEMON_SQUEEZY_*entries to avoid stale config.This will minimize subscriber disruption and keep your team aligned.
apps/web/utils/actions/premium.ts (1)
141-153:⚠️ Potential issueMulti-account seat upsell only works for Lemon Squeezy
When the premium row was created through Stripe,
lemonSqueezySubscriptionItemIdwill benull, so this branch blocks the user from buying extra seats even though the Stripe plan could absolutely be adjusted.Recommend mirroring the Stripe logic you added to
updateAccountSeats, eg.:- if (!premium.lemonSqueezySubscriptionItemId) { + if ( + !premium.lemonSqueezySubscriptionItemId && + !premium.stripeSubscriptionItemId + ) {…and add the matching
updateStripeSubscriptionItemQuantitycall.apps/web/utils/posthog.ts (2)
8-41: 🛠️ Refactor suggestionHandle missing env vars and HTTP errors in
getPosthogUserId
getPosthogUserIdassumes the PostHog env variables are present and that the fetch call succeeds.
If either assumption breaks, the function throws atresponseGet.json()or further down the chain, and callers can’t distinguish between “user not found” vs. “network/env failure”.+ if (!env.POSTHOG_API_SECRET || !env.POSTHOG_PROJECT_ID) { + logger.warn("PostHog env variables not set"); + return; + } ... - const resGet: { results: { id: string; distinct_ids: string[] }[] } = - await responseGet.json(); + if (!responseGet.ok) { + logger.error("PostHog persons lookup failed", { + status: responseGet.status, + email: options.email, + }); + return; + } + + const resGet: { results: { id: string; distinct_ids: string[] }[] } = + await responseGet.json();This early-exit pattern protects you from unhandled promise rejections and makes downstream logic easier to reason about.
74-97: 🛠️ Refactor suggestionAvoid creating a new
PostHogclient on every captureInstantiating a fresh client for each event spins up a new HTTPS connection, which is both slow and memory-heavy under high load.
A lightweight singleton (or module-level variable) is enough because thePostHogclient is already concurrency-safe.- const client = new PostHog(env.NEXT_PUBLIC_POSTHOG_KEY); + // Re-use a singleton to prevent connection thrashing + const client = + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore – stash on the global object + (globalThis.__posthogSingleton as PostHog | undefined) ?? + (globalThis.__posthogSingleton = new PostHog( + env.NEXT_PUBLIC_POSTHOG_KEY, + ));Remember to not call
shutdown()after every event when using a shared instance—only during graceful app termination.
🧹 Nitpick comments (23)
apps/web/ee/LICENSE.md (2)
4-4: Consider simplifying wording
The phrase “With regard to the Inbox Zero Software:” is somewhat wordy. You could shorten it to “Regarding the Inbox Zero Software:” or “For the Inbox Zero Software:” for improved readability.
38-38: Hyphenate compound adjective
The term “third party components” should be hyphenated as “third-party components” to conform to standard English usage.🧰 Tools
🪛 LanguageTool
[uncategorized] ~38-~38: If this is a compound adjective that modifies the following noun, use a hyphen.
Context: ...THER DEALINGS IN THE SOFTWARE. For all third party components incorporated into the Inbox ...(EN_COMPOUND_ADJECTIVE_INTERNAL)
apps/web/utils/actions/user.ts (1)
16-17: Potentially unused imports detectedTwo new Stripe-related imports have been added, but they don't appear to be used anywhere in this file. This could lead to unnecessary code bloat.
If these imports are not currently used in this file, consider removing them until they're needed. If they're intended for upcoming changes, please add a TODO comment explaining their purpose.
apps/web/utils/config.ts (1)
9-10: Consider documenting the constants with commentsThese new constants for knowledge base limits are critical for enforcing feature restrictions for non-premium users, but their purpose isn't immediately clear from just the variable names.
Add JSDoc comments to explain the purpose of these constants, especially noting that they represent limits for non-premium users:
+/** + * Maximum number of knowledge items allowed for non-premium users + */ export const KNOWLEDGE_BASIC_MAX_ITEMS = 1; +/** + * Maximum character count allowed per knowledge item for non-premium users + */ export const KNOWLEDGE_BASIC_MAX_CHARS = 2000;apps/web/app/api/google/watch/all/route.ts (1)
61-61: Consolidated access control logic.Simplified the conditional check to focus only on AI access, consistent with the broader refactoring of premium access controls.
However, the log message on line 62 still mentions "User does not have access to AI or cold email" despite the cold email check being removed. This should be updated for consistency.
- logger.info("User does not have access to AI or cold email", { + logger.info("User does not have access to AI", {apps/web/utils/actions/knowledge.ts (1)
65-76: Consider extracting premium check logic to reduce duplicationThe premium tier check logic is duplicated between
createKnowledgeActionandupdateKnowledgeAction. Consider extracting this into a reusable function to improve maintainability.+ // Add this function before the action definitions + async function checkContentLimits({ + userId, + content, + errorMessage + }: { + userId: string; + content: string; + errorMessage: string; + }) { + if (content.length > KNOWLEDGE_BASIC_MAX_CHARS) { + const hasAccess = await checkHasAccess({ + userId, + minimumTier: PremiumTier.BUSINESS_PLUS_MONTHLY, + }); + + if (!hasAccess) { + throw new SafeError(errorMessage); + } + } + } // Then in updateKnowledgeAction: - if (content.length > KNOWLEDGE_BASIC_MAX_CHARS) { - const hasAccess = await checkHasAccess({ - userId, - minimumTier: PremiumTier.BUSINESS_PLUS_MONTHLY, - }); - - if (!hasAccess) { - throw new SafeError( - `You can save up to ${KNOWLEDGE_BASIC_MAX_CHARS} characters to your knowledge base. Upgrade to a higher tier to save unlimited content.`, - ); - } - } + await checkContentLimits({ + userId, + content, + errorMessage: `You can save up to ${KNOWLEDGE_BASIC_MAX_CHARS} characters to your knowledge base. Upgrade to a higher tier to save unlimited content.`, + });apps/web/app/api/stripe/webhook/route.ts (1)
31-31: Use await withafter()or return the PromiseThe
after()function returns a Promise, but you're not awaiting it or returning it. This could lead to the function completing before the asynchronous processing is finished.- after(processEvent(event)); + return after(processEvent(event));apps/web/app/api/stripe/generate-checkout/route.ts (1)
67-68: Add more robust error handling for the response.The route returns a JSON response with the checkout, but doesn't handle potential errors in the response formatting.
- return NextResponse.json({ checkout }); + try { + return NextResponse.json({ + checkout, + url: checkout.url // Include the URL directly for easier client-side handling + }); + } catch (error) { + logger.error("Error creating checkout response", { error }); + return NextResponse.json( + { error: "Failed to create checkout session" }, + { status: 500 } + ); + }apps/web/prisma/schema.prisma (1)
187-187: Add documentation for emailAccountsAccess field.The purpose of this new field isn't immediately clear. Consider adding a comment explaining its usage and significance.
+ // Maximum number of email accounts allowed for this premium subscription emailAccountsAccess Int?apps/web/ee/billing/stripe/sync-stripe.ts (3)
16-25: Consider implementing pagination for subscriptions.While fetching only one subscription is likely sufficient for most customers, a more robust implementation would handle pagination for customers with multiple subscriptions.
const subscriptions = await stripe.subscriptions.list({ customer: customerId, limit: 1, status: "all", expand: [ "data.default_payment_method", "data.items.data.price", // Expand to get product ID ], }); + // If there are potentially more subscriptions, log this for awareness + if (subscriptions.has_more) { + logger.info("Customer has more than one subscription", { + customerId, + subscriptionCount: subscriptions.total_count + }); + }
79-102: Consider using a database transaction.The database update operation should ideally be wrapped in a transaction for atomicity.
- await prisma.premium.update({ + await prisma.$transaction(async (tx) => { + await tx.premium.update({ where: { stripeCustomerId: customerId }, data: { tier, stripeSubscriptionId: subscription.id, // ... rest of the fields }, }); + + // Additional related updates could be added here + });
86-86: Safer handling of product ID extraction.The current approach for handling the product ID could be more defensive.
- stripeProductId: typeof product === "string" ? product : product.id, // Handle expanded product object + stripeProductId: typeof product === "string" + ? product + : (product && 'id' in product) + ? product.id + : null, // More defensive handlingapps/web/prisma/migrations/20250506025728_stripe/migration.sql (1)
1-53: Consider adding a rollback plan.This migration makes significant schema changes. It would be helpful to have a rollback migration script in case of deployment issues.
Consider adding a comment with rollback instructions or creating a separate down migration script that would restore the previous schema state if needed.
apps/web/utils/premium/server.ts (1)
145-149: Log when no subscription item IDs are presentIf both
stripeSubscriptionItemIdandlemonSqueezySubscriptionItemIdarenull, the function silently exits and the caller has no clue why seats were not synced.
Consider adding anelsebranch with alogger.warnso ops can spot mis-configured premium rows.apps/web/utils/actions/premium.ts (2)
59-63:isPremiumagain ignores Stripe trialsSame concern as in
checkHasAccess: a user on a Stripe"trialing"subscription will fall through and burn free unsubscribe credits.
Make sure the business rule really intends to withhold premium perks during a trial.
408-482: Stripe checkout lacks acancel_urlWithout
cancel_url, a user who aborts the checkout is shown a Stripe error page instead of returning to your app.
Easy fix:- const checkout = await stripe.checkout.sessions.create({ + const checkout = await stripe.checkout.sessions.create({ customer: stripeCustomerId, + cancel_url: `${env.NEXT_PUBLIC_BASE_URL}/premium`,apps/web/app/api/lemon-squeezy/webhook/route.ts (2)
150-162: Split event tracking for clarityThe ternary mixes two very different calls. Using explicit
if/elsemakes future additions easier and is more readable (tiny nit).if (payload.data.attributes.status === "on_trial") { await trackTrialStarted(email, payload.data.attributes); } else { await trackUpgradedToPremium(email, payload.data.attributes); }
263-275: Batch database + tracking updates in one transaction
extendPremiumLemonupdates the DB, then an unrelated network call is made. If the PostHog call throws, the webhook returns 500 and Lemon will retry, leading to duplicate DB writes. Consider wrapping DB write + tracking in a transaction or at least catching errors around PostHog to keep idempotency.apps/web/app/(app)/premium/Pricing.tsx (1)
83-87:hasBothStripeAndLemonnaming is good, but watch the double-bang cast
!!(premium?.stripeSubscriptionId && premium?.lemonSqueezyCustomerId)works, yet the explicit Boolean cast is unnecessary and slightly reduces readability. Simple truthiness is enough:- const hasBothStripeAndLemon = !!( - premium?.stripeSubscriptionId && premium?.lemonSqueezyCustomerId - ); + const hasBothStripeAndLemon = + Boolean(premium?.stripeSubscriptionId) && + Boolean(premium?.lemonSqueezyCustomerId);Minor, feel free to ignore.
apps/web/app/(app)/premium/config.ts (3)
181-222: Plan display names don’t match internal enum names – can be confusing
businessTierName = "Pro"while it maps toPremiumTier.BUSINESS_*.
businessPlusTier.name = "Business"while it maps toPremiumTier.BUSINESS_PLUS_*.Marketing names that diverge from enum names are fine, but please add a comment or rename the enum (or constant) to prevent onboarding confusion for future devs.
112-123:tierToVariantIdincludesnullvalues – handle when serialisingFunctions consuming
tierToVariantIdshould expectnulland avoid insertingnullinto query strings or DB columns. A tiny helper like:export function tierHasLemonVariant(tier: PremiumTier): tier is Exclude<PremiumTier, BUSINESS_PLUS_* | LIFETIME> { return tierToVariantId[tier] !== null; }prevents accidental
?variant=nullbugs.
34-44: Spelling:pricingAdditonalEmail→pricingAdditionalEmailThe misspelling propagates throughout the file. Renaming now will spare future typos.
-export const pricingAdditonalEmail +export const pricingAdditionalEmailRemember to update all usages.
apps/web/utils/premium/index.ts (1)
58-69: Nit: variable shadowing & clarity inhasAiAccessThe local variable
hasAiAccessduplicates the function name and represents a boolean unrelated to AI per-se (it’s a general tier comparison). Consider renaming tohasRequiredTier(or similar) to improve readability and avoid future confusion.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (44)
apps/web/.env.example(0 hunks)apps/web/app/(app)/[emailAccountId]/automation/knowledge/KnowledgeBase.tsx(2 hunks)apps/web/app/(app)/[emailAccountId]/automation/knowledge/KnowledgeForm.tsx(3 hunks)apps/web/app/(app)/[emailAccountId]/settings/MultiAccountSection.tsx(1 hunks)apps/web/app/(app)/[emailAccountId]/usage/usage.tsx(1 hunks)apps/web/app/(app)/admin/AdminUpgradeUserForm.tsx(3 hunks)apps/web/app/(app)/admin/validation.tsx(1 hunks)apps/web/app/(app)/premium/Pricing.tsx(6 hunks)apps/web/app/(app)/premium/config.ts(7 hunks)apps/web/app/api/google/watch/all/route.ts(3 hunks)apps/web/app/api/google/webhook/process-history-item.test.ts(3 hunks)apps/web/app/api/google/webhook/process-history-item.ts(4 hunks)apps/web/app/api/google/webhook/process-history.ts(5 hunks)apps/web/app/api/google/webhook/types.ts(1 hunks)apps/web/app/api/lemon-squeezy/webhook/route.ts(9 hunks)apps/web/app/api/stripe/generate-checkout/route.ts(1 hunks)apps/web/app/api/stripe/success/route.ts(1 hunks)apps/web/app/api/stripe/webhook/route.ts(1 hunks)apps/web/app/api/user/complete-registration/route.ts(2 hunks)apps/web/app/api/user/me/route.ts(1 hunks)apps/web/components/PremiumAlert.tsx(3 hunks)apps/web/ee/LICENSE.md(1 hunks)apps/web/ee/billing/lemon/index.ts(1 hunks)apps/web/ee/billing/stripe/index.ts(1 hunks)apps/web/ee/billing/stripe/sync-stripe.ts(1 hunks)apps/web/env.ts(3 hunks)apps/web/package.json(2 hunks)apps/web/prisma/migrations/20250506025728_stripe/migration.sql(1 hunks)apps/web/prisma/schema.prisma(3 hunks)apps/web/scripts/addLemonDataToPosthog.ts(0 hunks)apps/web/scripts/encrypt-tokens.ts(0 hunks)apps/web/utils/actions/knowledge.ts(2 hunks)apps/web/utils/actions/premium.ts(9 hunks)apps/web/utils/actions/user.ts(1 hunks)apps/web/utils/config.ts(1 hunks)apps/web/utils/error.server.ts(2 hunks)apps/web/utils/middleware.ts(2 hunks)apps/web/utils/posthog.ts(2 hunks)apps/web/utils/premium/check-and-redirect-for-upgrade.tsx(1 hunks)apps/web/utils/premium/create-premium.ts(1 hunks)apps/web/utils/premium/index.ts(2 hunks)apps/web/utils/premium/server.ts(5 hunks)apps/web/utils/user/validate.ts(2 hunks)turbo.json(1 hunks)
💤 Files with no reviewable changes (3)
- apps/web/.env.example
- apps/web/scripts/encrypt-tokens.ts
- apps/web/scripts/addLemonDataToPosthog.ts
🧰 Additional context used
🧬 Code Graph Analysis (14)
apps/web/app/(app)/[emailAccountId]/usage/usage.tsx (1)
apps/web/utils/premium/index.ts (1)
isPremium(3-11)
apps/web/utils/error.server.ts (1)
apps/web/utils/posthog.ts (1)
trackError(132-146)
apps/web/app/api/user/complete-registration/route.ts (1)
apps/web/utils/posthog.ts (1)
trackUserSignedUp(99-108)
apps/web/app/(app)/admin/AdminUpgradeUserForm.tsx (1)
apps/web/utils/actions/premium.ts (1)
adminChangePremiumStatusAction(249-339)
apps/web/app/api/stripe/success/route.ts (3)
apps/web/utils/middleware.ts (1)
withAuth(186-188)apps/web/utils/posthog.ts (1)
trackStripeCheckoutCompleted(128-130)apps/web/ee/billing/stripe/sync-stripe.ts (1)
syncStripeDataToDb(8-111)
apps/web/app/(app)/[emailAccountId]/automation/knowledge/KnowledgeForm.tsx (6)
apps/web/app/api/knowledge/route.ts (1)
GetKnowledgeResponse(6-8)apps/web/providers/EmailAccountProvider.tsx (1)
useAccount(70-80)apps/web/components/PremiumAlert.tsx (1)
usePremium(14-42)apps/web/utils/premium/index.ts (1)
hasTierAccess(74-88)apps/web/utils/config.ts (1)
KNOWLEDGE_BASIC_MAX_ITEMS(9-9)apps/web/components/Alert.tsx (1)
AlertWithButton(28-58)
apps/web/components/PremiumAlert.tsx (1)
apps/web/utils/premium/index.ts (3)
isPremium(3-11)hasUnsubscribeAccess(51-56)hasAiAccess(58-72)
apps/web/utils/actions/knowledge.ts (4)
apps/web/utils/actions/safe-action.ts (1)
actionClient(43-73)apps/web/utils/config.ts (2)
KNOWLEDGE_BASIC_MAX_ITEMS(9-9)KNOWLEDGE_BASIC_MAX_CHARS(10-10)apps/web/utils/premium/server.ts (1)
checkHasAccess(166-201)apps/web/utils/error.ts (1)
SafeError(83-91)
apps/web/app/api/google/webhook/process-history.ts (1)
apps/web/utils/premium/index.ts (1)
hasAiAccess(58-72)
apps/web/app/api/google/webhook/process-history-item.ts (1)
apps/web/utils/premium/index.ts (1)
hasAiAccess(58-72)
apps/web/utils/actions/premium.ts (9)
apps/web/utils/logger.ts (1)
createScopedLogger(17-65)apps/web/utils/premium/index.ts (1)
isPremium(3-11)apps/web/utils/premium/create-premium.ts (1)
createPremiumForUser(3-10)apps/web/utils/premium/server.ts (2)
upgradeToPremiumLemon(14-62)cancelPremiumLemon(81-121)apps/web/utils/actions/safe-action.ts (2)
adminActionClient(90-99)actionClientUser(76-88)apps/web/app/(app)/premium/config.ts (1)
getStripePriceId(102-108)apps/web/ee/billing/stripe/index.ts (1)
getStripe(9-23)apps/web/utils/error.ts (1)
SafeError(83-91)apps/web/utils/posthog.ts (2)
trackStripeCustomerCreated(110-122)trackStripeCheckoutCreated(124-126)
apps/web/utils/premium/check-and-redirect-for-upgrade.tsx (1)
apps/web/utils/premium/index.ts (1)
isPremium(3-11)
apps/web/utils/premium/server.ts (5)
apps/web/utils/logger.ts (1)
createScopedLogger(17-65)apps/web/ee/billing/stripe/index.ts (1)
updateStripeSubscriptionItemQuantity(25-57)apps/web/ee/billing/lemon/index.ts (1)
updateSubscriptionItemQuantity(21-31)apps/web/utils/error.ts (1)
SafeError(83-91)apps/web/utils/premium/index.ts (2)
isPremium(3-11)hasTierAccess(74-88)
apps/web/app/(app)/premium/config.ts (1)
apps/web/env.ts (1)
env(5-203)
🪛 LanguageTool
apps/web/ee/LICENSE.md
[style] ~3-~3: ‘With regard to’ might be wordy. Consider a shorter alternative.
Context: ...ight (c) 2025-present Inbox Zero, Inc. With regard to the Inbox Zero Software: This software...
(EN_WORDINESS_PREMIUM_WITH_REGARD_TO)
[uncategorized] ~38-~38: If this is a compound adjective that modifies the following noun, use a hyphen.
Context: ...THER DEALINGS IN THE SOFTWARE. For all third party components incorporated into the Inbox ...
(EN_COMPOUND_ADJECTIVE_INTERNAL)
🔇 Additional comments (62)
apps/web/ee/LICENSE.md (2)
1-2: New license file added
The title and copyright notice clearly identify the “Inbox Zero Commercial License” and its effective date.
30-37: Standard warranty disclaimer
The “AS IS” warranty and liability clauses (lines 30–37) follow a common, acceptable template. No changes needed here.apps/web/app/api/user/me/route.ts (1)
23-24: LGTM: Stripe subscription fields correctly addedThe addition of Stripe subscription fields to the user data selection supports the new payment processor integration.
apps/web/package.json (1)
65-65:Details
❓ Verification inconclusive
Stripe dependencies correctly added
The addition of both client-side and server-side Stripe packages supports the new payment processor integration.
Also applies to: 137-137
🌐 Web query:
What are the latest versions of @stripe/stripe-js and stripe npm packages?💡 Result:
As of May 8, 2025, the latest versions of the Stripe-related npm packages are:
@stripe/stripe-js: Version 6.1.0, published 3 days ago. (npmjs.com)
stripe: Version 17.7.0, published 21 days ago. (npmjs.com)
These versions include the most recent updates and features for integrating Stripe into your JavaScript applications.
Citations:
- 1: https://www.npmjs.com/package/%40stripe/stripe-js?utm_source=openai
- 2: https://www.npmjs.com/package/stripe?utm_source=openai
🏁 Script executed:
#!/bin/bash # List all Stripe-related dependencies with line numbers in apps/web/package.json rg -nE '"@stripe/stripe-js"|"stripe"' apps/web/package.jsonLength of output: 127
🏁 Script executed:
#!/bin/bash # Search for Stripe-related dependencies in all package.json files find . -type f -name package.json -exec grep -E -Hn '"@stripe/stripe-js"|"stripe"' {} \;Length of output: 206
Confirm Stripe package versions
It appears you’ve pinned
@stripe/stripe-jsto 7.3.0 (apps/web/package.json:65)stripeto 18.1.0 (apps/web/package.json:137)However, the npm registry currently lists
- @stripe/stripe-js@6.1.0
- stripe@17.7.0
Please verify that 7.3.0 and 18.1.0 are published (e.g. on a private registry or as pre-releases) and are the intended versions before merging.
apps/web/app/(app)/[emailAccountId]/usage/usage.tsx (1)
25-28: Implementation correctly updates to support Stripe billingThe code now passes both Lemon Squeezy renewal date and Stripe subscription status to the
isPremiumfunction, aligning with the migration to support Stripe as a payment processor alongside Lemon Squeezy.apps/web/ee/billing/lemon/index.ts (1)
47-54: Good approach to comment out rather than delete the Lemon Squeezy switching functionalityCommenting out the
switchPremiumPlanfunction rather than deleting it is a good transitional approach while migrating from Lemon Squeezy to Stripe. This maintains the code for reference or potential backward compatibility.apps/web/app/(app)/admin/AdminUpgradeUserForm.tsx (3)
8-8: Updated to use the admin-specific premium status actionThe code now uses
adminChangePremiumStatusActionwhich is specifically designed for admin-level operations, improving role separation and security.
20-20: Action implementation consistently updatedThis line correctly updates the action being executed to match the imported admin-specific action.
94-101: New premium tiers correctly addedThe addition of BUSINESS_PLUS_ANNUALLY and BUSINESS_PLUS_MONTHLY tiers aligns with the updated premium tier offerings. These new options expand the subscription choices available to users.
apps/web/utils/error.server.ts (2)
2-2: Updated to use more specialized error tracking functionThe import is now using the more specialized
trackErrorfunction from PostHog utilities, which is part of the broader refactoring of event tracking across the application.
17-17: Improved error tracking with consolidated parametersThe implementation now uses the more specialized
trackErrorfunction with a consolidated object parameter containing all error metadata. This provides better structure and consistency in error reporting.apps/web/app/(app)/admin/validation.tsx (1)
15-16: Added support for new BUSINESS_PLUS tiersThe validation schema now correctly includes the new BUSINESS_PLUS_MONTHLY and BUSINESS_PLUS_ANNUALLY premium tiers, which aligns with the introduction of Stripe as a new payment processor.
apps/web/utils/premium/create-premium.ts (1)
3-10: Good utility for premium record creationThis new utility function is well-structured and focused on a single responsibility - creating a premium record for a user. It correctly establishes the database relationships by connecting the user as both a user and an admin.
apps/web/utils/middleware.ts (2)
60-63: Properly handling Next.js redirectsThis error handling improvement correctly allows Next.js redirects to propagate without being caught by the generic error handler. This is crucial for Stripe checkout flows where redirects are common.
141-141: Simplified user ID extractionGood simplification of the user ID assignment, making the code more concise and readable.
apps/web/app/api/google/webhook/types.ts (1)
12-12: Consolidated access flags into single hasAiAccess propertyThis change appropriately simplifies the access control model by consolidating previously separate flags (hasColdEmailAccess and hasAiAutomationAccess) into a single hasAiAccess flag. This aligns with the shift toward tier-based access control in the premium subscription model.
apps/web/app/(app)/[emailAccountId]/automation/knowledge/KnowledgeBase.tsx (2)
30-31: Clean import reorganization.Reordering imports for better organization without changing functionality.
70-70: Good addition of the knowledge item count for premium tier validation.Passing the total count of knowledge items to the form enables premium tier access enforcement, ensuring users don't exceed their allowed knowledge base entries based on their subscription tier.
apps/web/app/api/user/complete-registration/route.ts (2)
6-6: Improved event tracking with semantic function.Switched from generic PostHog capture to a specialized tracking function, enhancing analytics clarity.
96-96: Simplified event tracking implementation.Now using the specialized
trackUserSignedUpfunction that encapsulates the event details and properties, making the code more maintainable.apps/web/app/api/google/watch/all/route.ts (3)
8-8: Simplified import for premium access.Removed unused
hasColdEmailAccessimport as part of the consolidation of premium access checks.
39-40: Streamlined premium data selection.Now only retrieving the
tierfield from premium data, which is sufficient for the new tier-based access control system.
57-58: Simplified premium access check parameters.Updated to pass only the tier value to
hasAiAccessinstead of feature-specific flags, reflecting the migration to tier-based access control.apps/web/app/api/google/webhook/process-history-item.test.ts (2)
104-104: Access flag consolidation looks good.The change from separate flags (
hasColdEmailAccessandhasAiAutomationAccess) to a unifiedhasAiAccessflag aligns with the broader refactoring to simplify access control based on premium tiers.
199-199: Consistent flag consolidation across test cases.Consistently updating test cases to use the consolidated
hasAiAccessflag ensures that tests properly reflect the new access control model.Also applies to: 233-233
apps/web/utils/premium/check-and-redirect-for-upgrade.tsx (2)
19-24: Good addition of Stripe subscription status.Adding the Stripe subscription status to the Prisma query is essential for the new payment processor integration.
31-36: Correctly updated isPremium call with Stripe support.The isPremium function call now properly includes both Lemon Squeezy renewal date and Stripe subscription status, maintaining backward compatibility while enabling the new payment processor.
apps/web/utils/user/validate.ts (2)
24-24: Good migration to tier-based access model.Changed premium selection from
aiAutomationAccesstotier, aligning with the new tier-based access control system.
33-36:Details
✅ Verification successful
Correctly updated hasAiAccess call with tier-based parameter.
The
hasAiAccesscall now uses the premium tier instead of the specific feature flag, which supports the consolidated access control model.Let me verify that the
hasAiAccessfunction has been updated to accept a tier parameter:
🏁 Script executed:
#!/bin/bash # Check the signature of hasAiAccess function to ensure it accepts tier parameter rg -A 5 "export const hasAiAccess" --glob "*/premium/*.ts"Length of output: 209
Let’s search for the
hasAiAccessdefinition across all TS files:
🏁 Script executed:
#!/bin/bash # Search for hasAiAccess definition across all TypeScript files rg "export const hasAiAccess" -A5 -t ts rg "export function hasAiAccess" -A5 -t tsLength of output: 398
hasAiAccess call updated correctly
The
hasAiAccesshelper in apps/web/utils/premium/index.ts now has the signature:export const hasAiAccess = ( tier: PremiumTier | null, aiApiKey?: string | null, ) => { … }and the invocation in apps/web/utils/user/validate.ts lines 33–36 correctly passes
emailAccount.user.premium?.tier || null, emailAccount.user.aiApiKeyThis aligns with our consolidated tier-based access model.
turbo.json (2)
59-65: Properly added Stripe environment variables.Added the necessary Stripe-related environment variables for the build task, which is essential for the new payment processor integration.
66-74: Maintained backward compatibility with Lemon Squeezy.While transitioning to Stripe, you've maintained the Lemon Squeezy store ID and variant IDs to support existing customers and ensure backward compatibility.
apps/web/components/PremiumAlert.tsx (3)
7-7: Cleaner imports to support the new premium access model.This import change reflects the consolidation of premium access checks, removing the unused
hasColdEmailAccessimport while retaining the necessary utility functions.
21-24: Good implementation of dual payment processor support.The premium check now properly evaluates both Lemon Squeezy renewal dates and Stripe subscription status, aligning with the integration of Stripe as a new payment processor.
36-38: Feature flags replaced with tier-based access control.The implementation now consistently uses tier-based logic rather than individual feature flags, which simplifies the codebase and makes premium access decisions more maintainable. The
hasUnsubscribeAccessandhasAiAccessutility functions provide a unified approach to determining feature access.apps/web/app/(app)/[emailAccountId]/automation/knowledge/KnowledgeForm.tsx (4)
5-31: Proper imports added for premium tier restriction UI.All necessary imports have been added to support the premium tier gating functionality, including UI components for the alert, premium utilities, and configuration constants.
37-38: New prop to support knowledge item count restriction.The addition of the
knowledgeItemsCountprop allows the component to receive the current count from its parent, enabling proper enforcement of tier-based limitations.Also applies to: 42-43
45-50: Well-implemented tier access check.The implementation correctly uses the
hasTierAccessutility with theBUSINESS_PLUS_MONTHLYminimum tier requirement to determine if the user has full access to knowledge base functionality.
108-126: Clear upgrade prompt for knowledge base limits.The conditional alert is well-implemented and provides a clear path for users to upgrade when they reach their knowledge item limit. The alert only appears when needed (creating a new item, not editing an existing one, and reached the limit).
apps/web/app/api/google/webhook/process-history.ts (3)
58-59: Updated premium selection to support Stripe.The user premium data selection now includes
stripeSubscriptionStatusandtier, replacing legacy feature flags and aligning with the new subscription model.
72-74: Dual payment processor support in premium check.The
isPremiumfunction call now properly includes the Stripe subscription status alongside Lemon Squeezy renewal dates, ensuring correct premium status determination.
93-96: Simplified access control with unified AI access check.The code now uses a single
hasAiAccesscheck based on the user's premium tier and AI API key, replacing the previous separate checks for cold email access and AI automation access. This streamlines the logic and makes the code more maintainable.apps/web/app/api/google/webhook/process-history-item.ts (4)
37-37: Consolidated access flag in function parameters.The function parameters have been simplified by replacing multiple access flags (
hasColdEmailAccess,hasAiAutomationAccess) with a singlehasAiAccessparameter, streamlining the interface.
138-138: Updated cold email blocker check.The
shouldRunColdEmailBlockerfunction call now correctly uses the unifiedhasAiAccessflag instead of a separate cold email access check.
180-180: Simplified automation rules execution condition.The condition for running automation rules now directly uses the
hasAiAccessflag, making the code more readable and consistent with the new access control model.
262-265: Simplified cold email blocker function signature.The
shouldRunColdEmailBlockerfunction has been updated to use the singlehasAiAccessparameter, consistent with the consolidated access control approach throughout the codebase.Also applies to: 270-271
apps/web/ee/billing/stripe/index.ts (1)
25-57: Good implementation of subscription quantity update with safety mechanismsThe function correctly implements:
- A minimum quantity of 1 (preventing zero or negative quantities)
- Proper error handling with detailed logging
- Input validation for required parameters
apps/web/app/api/stripe/success/route.ts (1)
11-18: Good use ofafter()for non-blocking analyticsUsing Next.js's
after()function to handle PostHog tracking is a good practice as it doesn't block the response to the user.apps/web/utils/actions/knowledge.ts (1)
26-45: Good implementation of premium tier checks for knowledge itemsThe premium tier verification correctly enforces limits on non-premium users while allowing premium users to exceed these limits. The error message is clear and actionable.
apps/web/app/api/stripe/generate-checkout/route.ts (1)
1-11: Imports look good.The imports cover all necessary dependencies including Next.js server components, Stripe utilities, authentication middleware, database access, and tracking functions.
apps/web/prisma/schema.prisma (3)
172-184: Well-structured Stripe fields in Premium model.The added Stripe fields have appropriate types and constraints. The unique constraints on ID fields are particularly important to prevent duplicate records.
641-642: Business Plus tiers added correctly.The new premium tiers follow the established naming convention and fit logically into the existing enum structure.
680-680: Stripe processor type added correctly.Adding STRIPE to the ProcessorType enum is a necessary change for the new payment integration.
apps/web/ee/billing/stripe/sync-stripe.ts (1)
1-12: Function signature and imports look good.The imports and function signature are well-structured with appropriate types.
apps/web/prisma/migrations/20250506025728_stripe/migration.sql (5)
1-11: Migration warnings are appropriately descriptive.The migration includes proper warnings about dropping columns and adding unique constraints, which is important for awareness during deployment.
19-25: Enum additions follow best practices.Adding the new premium tiers and the STRIPE processor type follows SQL best practices for enum modifications.
26-40: Table alterations are comprehensive.The migration correctly drops deprecated feature access columns and adds all necessary Stripe-related fields with appropriate types.
42-43: Removing FeatureAccess enum is appropriate.Dropping the FeatureAccess enum is a clean way to remove the old access control system now that it's being replaced with tier-based access.
45-52: Unique indexes are correctly implemented.Creating unique indexes on Stripe identifier fields ensures data integrity at the database level.
apps/web/utils/actions/premium.ts (1)
445-456: Double-check availability ofafter()helper
afterwas introduced in Next 14.2 and is still experimental. If your deployment target is older or edge runtimes, this import will crash the action.Please verify the runtime supports it or fall back to a plain
try/finally.apps/web/env.ts (1)
151-159: Keepexperimental__runtimeEnvlist in sync withclientschemaYou added four Stripe keys to
client, good. Make sure any future additions are reflected here; otherwise they are tree-shaken away on the client bundle.apps/web/app/(app)/premium/Pricing.tsx (1)
311-318: Verify that passingtierto the Stripe Billing Portal is supported
getBillingPortalUrlAction({ tier: upgradeToTier })Stripe’s customer-portal API does not accept a price-id/plan switch; upgrades are performed inside the portal UI. Ensure your server action ignores/handles this param gracefully; otherwise remove it to avoid confusion.
apps/web/utils/premium/index.ts (1)
38-49: Verify continued need for deprecatedLIFETIMEtier intierRankingThe PR description states that the
LIFETIMEtier was removed, yet it’s still ranked here.
Keeping stale keys can lead to mismatched rank comparisons and dead code paths. Please verify and delete or mark as legacy if truly obsolete.
| <Button type="submit" loading={isExecuting}> | ||
| Save | ||
| </Button> |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
Simplified UI for Stripe integration.
Removed the conditional Lemon Squeezy payment button in favor of a simple "Save" button, aligning with the migration to Stripe-based billing.
Note that the form submission still blocks if extra seats are needed (line 176), but no longer provides a direct purchase option. Ensure this doesn't confuse users who need to upgrade but aren't given a clear path to do so.
🏁 Script executed:
#!/bin/bash
# Check if there's another mechanism for users to upgrade when they need more seats
# Looking for upgrade modals or navigation in the codebase
# Search for places that might handle the upgrade flow for users needing more seats
rg -A 3 "needsToPurchaseMoreSeats" --type=tsx --type=tsLength of output: 83
🏁 Script executed:
#!/bin/bash
# Locate the MultiAccountSection component and inspect the block around line 176
file=$(fd -e tsx MultiAccountSection.tsx)
echo "Inspecting: $file"
echo "----- Lines 160–200 -----"
sed -n '160,200p' "$file"
echo
echo "----- Searching for upgrade, billing, or seat keywords in this file -----"
rg -n -C3 'upgrade|billing|seat' "$file"Length of output: 2727
Provide a clear upgrade path when seat limit is reached
The “Save” button now replaces the old Lemon Squeezy purchase flow for Stripe migration. We verified that:
- In the
onSubmitcallback (apps/web/app/(app)/[emailAccountId]/settings/MultiAccountSection.tsx lines 168–174), submission simply returns ifneedsToPurchaseMoreSeatsis true. - The
ExtraSeatsAlertcomponent (same file, lines 220–228) only renders a static alert showing current seats without offering any action.
As a result, users who hit the seat limit have no obvious way to upgrade. Please add an explicit upgrade option, for example:
• In the onSubmit block, redirect to your billing page or open an “Upgrade plan” modal when needsToPurchaseMoreSeats is true.
• Enhance ExtraSeatsAlert to include an “Upgrade plan” button or link alongside the seat information.
apps/web/utils/posthog.ts
Outdated
| export async function trackStripeEvent(email: string, event: Stripe.Event) { | ||
| return posthogCaptureEvent(email, "Stripe event", event); | ||
| } |
There was a problem hiding this comment.
Sending the raw Stripe.Event risks oversized payloads & PII leakage
Stripe.Event objects can be hundreds of KB and frequently contain e-mails, addresses, and card metadata. PostHog truncates large payloads and may silently drop the event, and you may inadvertently store sensitive data in analytics.
-export async function trackStripeEvent(email: string, event: Stripe.Event) {
- return posthogCaptureEvent(email, "Stripe event", event);
+export async function trackStripeEvent(
+ email: string,
+ event: Stripe.Event,
+) {
+ const { id, type, api_version } = event;
+ return posthogCaptureEvent(email, "Stripe event", {
+ id,
+ type,
+ api_version,
+ object: event.data?.object?.object, // e.g. 'invoice'
+ });
}Limit the payload to non-sensitive, high-level fields to stay under PostHog’s 512 kB limit and comply with GDPR/PCI obligations.
📝 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.
| export async function trackStripeEvent(email: string, event: Stripe.Event) { | |
| return posthogCaptureEvent(email, "Stripe event", event); | |
| } | |
| export async function trackStripeEvent( | |
| email: string, | |
| event: Stripe.Event, | |
| ) { | |
| const { id, type, api_version } = event; | |
| return posthogCaptureEvent(email, "Stripe event", { | |
| id, | |
| type, | |
| api_version, | |
| object: event.data?.object?.object, // e.g. 'invoice' | |
| }); | |
| } |
There was a problem hiding this comment.
Actionable comments posted: 15
🔭 Outside diff range comments (2)
apps/web/utils/premium/server.ts (1)
107-113:⚠️ Potential issueRemoved columns still referenced – this will crash after migration
bulkUnsubscribeAccess,aiAutomationAccess, andcoldEmailBlockerAccesswere dropped from thePremiumtable. Updating them here will raisePrismaClientKnownRequestError (P2022: Column does not exist)after you run the migration.- ...(expired - ? { - bulkUnsubscribeAccess: null, - aiAutomationAccess: null, - coldEmailBlockerAccess: null, - } - : {}), + // legacy feature columns were removed – nothing extra to updateapps/web/utils/posthog.ts (1)
74-97: 🛠️ Refactor suggestionCreating a new
PostHogclient per call is wasteful and can throttle event delivery.
posthogCaptureEventinstantiates and shuts down a client for every single event. Under load this leads to:
- TCP/TLS hand-shakes on each call.
- Possible rate-limiting / dropped events.
- Higher latency for the calling API.
Prefer a singleton that is initialised once per process:
-const client = new PostHog(env.NEXT_PUBLIC_POSTHOG_KEY); +// module-level singleton +const posthogClient = + globalThis.__posthogClient || + (globalThis.__posthogClient = new PostHog(env.NEXT_PUBLIC_POSTHOG_KEY)); ... - const client = new PostHog(env.NEXT_PUBLIC_POSTHOG_KEY); - client.capture({ ... }); - await client.shutdown(); + posthogClient.capture({ ... });Shut the client down in a global SIGTERM/
process.on("beforeExit")handler so Lambda/Edge runtimes still flush buffers cleanly.
🧹 Nitpick comments (25)
apps/web/ee/LICENSE.md (2)
4-4: Simplify wording for conciseness.
The phrase “With regard to the Inbox Zero Software:” is wordy. Consider replacing it with a more concise “Regarding the Inbox Zero Software:”.Apply this diff:
- With regard to the Inbox Zero Software: + Regarding the Inbox Zero Software:
38-39: Hyphenate compound adjective.
The term “third party components” should be hyphenated as “third-party components” to improve readability and adhere to style guidelines.Apply this diff:
- For all third party components incorporated into the Inbox Zero Software, those + For all third-party components incorporated into the Inbox Zero Software, those🧰 Tools
🪛 LanguageTool
[uncategorized] ~38-~38: If this is a compound adjective that modifies the following noun, use a hyphen.
Context: ...THER DEALINGS IN THE SOFTWARE. For all third party components incorporated into the Inbox ...(EN_COMPOUND_ADJECTIVE_INTERNAL)
apps/web/ee/billing/lemon/index.ts (1)
47-54: Consider removing commented-out codeThis commented-out code appears to be part of the transition from Lemon Squeezy to Stripe. While it's useful to keep code temporarily during a transition, commented code adds noise to the codebase long-term.
If this function is no longer needed due to the Stripe migration, consider removing it entirely and relying on version control history if you need to reference it in the future.
apps/web/app/api/stripe/generate-checkout/route.ts (1)
65-65: Consider adding more context to the tracking event.The tracking event for checkout creation could include more context such as the checkout session ID for better analytics.
-after(() => trackStripeCheckoutCreated(user.email)); +after(() => trackStripeCheckoutCreated(user.email, checkout.id));apps/web/app/api/google/webhook/process-history-item.ts (1)
264-271: Micro-refactor: compress the repeated enum checksThe three explicit comparisons are correct but verbose and easy to forget when a new
ColdEmailSettingis added. A small refactor keeps the intent clear and DRY:-return ( - (coldEmailBlocker === ColdEmailSetting.ARCHIVE_AND_READ_AND_LABEL || - coldEmailBlocker === ColdEmailSetting.ARCHIVE_AND_LABEL || - coldEmailBlocker === ColdEmailSetting.LABEL) && - hasAiAccess -); +const COLD_EMAIL_ACTIONS = new Set([ + ColdEmailSetting.ARCHIVE_AND_READ_AND_LABEL, + ColdEmailSetting.ARCHIVE_AND_LABEL, + ColdEmailSetting.LABEL, +]); + +return COLD_EMAIL_ACTIONS.has(coldEmailBlocker ?? null) && hasAiAccess;apps/web/components/PremiumAlert.tsx (1)
21-24: Minor:isUserPremiumcan be simplified
isPremiumalready foldsnullhandling; the extrapremium && …wrapper makes the intent slightly harder to scan.- const isUserPremium = !!( - premium && - isPremium(premium.lemonSqueezyRenewsAt, premium.stripeSubscriptionStatus) - ); + const isUserPremium = isPremium( + premium?.lemonSqueezyRenewsAt ?? null, + premium?.stripeSubscriptionStatus ?? null, + );Purely a readability tweak—feel free to skip if you prefer the current explicitness.
apps/web/app/api/stripe/success/route.ts (1)
25-29: Potential latency: heavy sync before redirect
syncStripeDataToDbmay hit Stripe’s API, which can add hundreds of ms to the response.
You’re already usingafter; moving the sync into that callback keeps the page load snappy while still ensuring the DB is updated soon after:- await syncStripeDataToDb({ customerId: user.premium.stripeCustomerId }); - - redirect("/setup"); + after(() => + syncStripeDataToDb({ customerId: user.premium!.stripeCustomerId }).catch( + (e) => console.error("Stripe sync failed", e), + ), + ); + + redirect("/setup");Optional, but worth considering for perceived performance.
apps/web/app/api/stripe/webhook/route.ts (3)
65-74: Improve error message for invalid customer ID.When the customer ID isn't a string, the error message could be more specific about which field is missing or invalid.
- throw new Error(`ID isn't string.\nEvent type: ${event.type}`); + throw new Error(`Customer ID invalid or missing in Stripe event.\nEvent type: ${event.type}\nObject type: ${(event?.data?.object as any)?.object || 'unknown'}`);
76-79: Consider adding error recovery for failed operations.Using
Promise.allSettledcorrectly allows operations to proceed independently, but there's no handling for individual failures. Consider adding error logging for failed settlements.- return await Promise.allSettled([ - syncStripeDataToDb({ customerId }), - trackEvent(customerId, event), - ]); + const results = await Promise.allSettled([ + syncStripeDataToDb({ customerId }), + trackEvent(customerId, event), + ]); + + results.forEach((result, index) => { + if (result.status === 'rejected') { + const operation = index === 0 ? 'syncStripeDataToDb' : 'trackEvent'; + logger.error(`${operation} failed`, { + error: result.reason, + customerId, + eventType: event.type + }); + } + }); + + return results;
82-89: Potential performance improvement for user lookup.The database query could be optimized to only select the email field instead of the entire users array.
const user = await prisma.premium.findUnique({ where: { stripeCustomerId: customerId }, - select: { users: { select: { email: true } } }, + select: { users: { select: { email: true }, take: 1 } }, });apps/web/ee/billing/stripe/sync-stripe.ts (1)
79-102: Improved timestamp handling and field structure.The code properly converts Unix timestamps to JavaScript Date objects and handles both string and object product references. However, there's one area for improvement:
The
current_period_endis being read fromsubscriptionItembut should likely come from the subscription itself:- stripeRenewsAt: subscriptionItem.current_period_end // RenewsAt uses the item's period end + stripeRenewsAt: subscription.current_period_end // RenewsAt uses the subscription's period endDouble-check this with the Stripe API documentation to ensure you're using the right field.
apps/web/utils/actions/knowledge.ts (2)
40-44: Error message could be more clear about the required premium tier.The error message tells users they need to upgrade to a "higher tier" but doesn't specify which tier is needed.
- throw new SafeError( - `You can save up to ${KNOWLEDGE_BASIC_MAX_CHARS} characters and ${KNOWLEDGE_BASIC_MAX_ITEMS} item to your knowledge base. Upgrade to a higher tier to save unlimited content.`, - ); + throw new SafeError( + `You can save up to ${KNOWLEDGE_BASIC_MAX_CHARS} characters and ${KNOWLEDGE_BASIC_MAX_ITEMS} items to your knowledge base. Upgrade to Business Plus to save unlimited content.`, + );
71-75: Similar improvement for update error message.The update error message has the same issue of not specifying which tier is required.
- throw new SafeError( - `You can save up to ${KNOWLEDGE_BASIC_MAX_CHARS} characters to your knowledge base. Upgrade to a higher tier to save unlimited content.`, - ); + throw new SafeError( + `You can save up to ${KNOWLEDGE_BASIC_MAX_CHARS} characters to your knowledge base. Upgrade to Business Plus to save unlimited content.`, + );apps/web/prisma/migrations/20250506025728_stripe/migration.sql (1)
46-52: Consider partial unique indexes for nullable Stripe IDs
stripeCustomerId,stripeSubscriptionId, andstripeSubscriptionItemIdare all nullable. Postgres allows multipleNULLrows in a standard UNIQUE index, so the constraint does not guarantee uniqueness until a value is present.
If you need real uniqueness only when the column is not null, define the index as:CREATE UNIQUE INDEX … ON "Premium"(stripeCustomerId) WHERE stripeCustomerId IS NOT NULL;Same for the other two columns.
apps/web/utils/premium/server.ts (1)
186-195: Stripe “trialing” & “incomplete” statuses not considered
isPremiumonly treats"active"as premium. Stripe marks new subscriptions"trialing"by default (you create them with a 7-day trial ingenerateCheckoutSessionAction). During that periodcheckHasAccesswill deny access even though the user has an active subscription.Extend
isPremiumto include"trialing"(and optionally"past_due") or make the set configurable.apps/web/app/(app)/premium/Pricing.tsx (4)
99-116: Open external billing portal in a new tab with safer attributes
window.open(url)inherits the browsing context causingwindow.openerleaks.
Prefer:- window.open(url); + window.open(url, "_blank", "noopener,noreferrer");…or switch to
<Link target="_blank" rel="noopener noreferrer">for consistency with the Lemon Squeezy button just below.
192-194: “Save up to 20 %” badge is hard-codedThe badge is static even though the discount percentages are calculated per tier.
Consider deriving the figure dynamically fromtiersto avoid future drift when marketing changes.
298-354: Race-condition / double-click protectionThere is no guard against a user double-clicking the CTA while
loadingis alreadytrue.
Add an early return to avoid spamming Stripe / backend:onClick={async () => { - setLoading(true); + if (loading) return; // ignore subsequent clicks + setLoading(true);
227-233: Edge case:userPremiumTiermay benull
getCTAText()accessesuserPremiumTierwithout null-check when the user is not logged-in/premium. The current logic works, but the boolean-to-string coercion can be clearer:- if (userPremiumTier) return "Switch to this plan"; + if (userPremiumTier !== null) return "Switch to this plan";apps/web/app/(app)/premium/config.ts (2)
20-31: Dot-notation on dynamic enum keys may break after enum renaming
pricing.BUSINESS_MONTHLYrelies on the enum’s string representation to equal the property name.
If Prisma ever changes the enum values (e.g. to “BUSINESS_MONTHLY_TIER”), TypeScript will not warn you and runtime will returnundefined.Prefer bracket access with the enum constant to guarantee alignment:
price: { monthly: pricing[PremiumTier.BUSINESS_MONTHLY], ... }Repeat for all similar occurrences (
pricingAdditonalEmail, discounts, etc.).
220-264: Duplicate feature lists between Pro and Business PlusMost feature arrays are copy-pasted with only one or two differences.
Consider extracting common features to reduce maintenance overhead:const COMMON_FEATURES = [ aiAssistantFeature, replyZeroFeature, coldEmailBlockerFeature, bulkUnsubscribeFeature, analyticsFeature, { text: "Unlimited AI credits" }, ]; const businessTier: Tier = { ...features: COMMON_FEATURES.concat(basicKb) } const businessPlusTier: Tier = { ...features: COMMON_FEATURES.concat(unlimitedKb, prioritySupport) }apps/web/env.ts (1)
101-106: Duplicate env validation for client & runtimeEnvEvery new Stripe price ID is declared twice (client schema and
experimental__runtimeEnv).
Consider creating a helper that derives the runtime object from the client schema keys to avoid drift when adding future variables.apps/web/app/api/lemon-squeezy/webhook/route.ts (1)
150-163: PostHog event selection duplicates business logic – consider an explicit guard instead of ternary.The ternary choosing between
trackTrialStartedandtrackUpgradedToPremiumrelies on the raw"on_trial"status string scattered throughout the codebase. A small typo would silently route the event to the wrong tracker.- await Promise.allSettled([ - payload.data.attributes.status === "on_trial" - ? trackTrialStarted(email, payload.data.attributes) - : trackUpgradedToPremium(email, payload.data.attributes), + const { status } = payload.data.attributes; + await Promise.allSettled([ + status === "on_trial" + ? trackTrialStarted(email, payload.data.attributes) + : trackUpgradedToPremium(email, payload.data.attributes), upgradedToPremium(email, tier), ])Extracting
statusonce (or enumerating allowed statuses in a helper) prevents string drift and improves readability.apps/web/utils/posthog.ts (1)
263-265: Full Stripe event objects may exceed PostHog’s 1 MB property limit.
trackStripeEventforwards the entireStripe.Eventobject. Large nested objects (e.g.invoice.lines) can silently drop the capture or truncate properties. Consider whitelisting a minimal subset instead:export async function trackStripeEvent(email: string, event: Stripe.Event) { const { id, type, data } = event; return posthogCaptureEvent(email, "Stripe event", { id, type, object: data?.object?.object, // e.g. "invoice" }); }This keeps analytics compact and consistent.
apps/web/utils/premium/index.ts (1)
59-69:hasAiAccessrelies on duplicated constants – could be simplified withhasTierAccess.
hasAiAccessrepeats ranking comparisons already abstracted byhasTierAccess.- const ranking = tierRanking[tier]; - const hasAiAccess = !!( - ranking >= tierRanking[PremiumTier.BUSINESS_MONTHLY] || - (ranking >= tierRanking[PremiumTier.PRO_MONTHLY] && aiApiKey) - ); + const hasAiAccess = + hasTierAccess({ tier, minimumTier: PremiumTier.BUSINESS_MONTHLY }) || + (aiApiKey && + hasTierAccess({ tier, minimumTier: PremiumTier.PRO_MONTHLY }));This avoids future drift when the ranking table changes.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (44)
apps/web/.env.example(0 hunks)apps/web/app/(app)/[emailAccountId]/automation/knowledge/KnowledgeBase.tsx(2 hunks)apps/web/app/(app)/[emailAccountId]/automation/knowledge/KnowledgeForm.tsx(3 hunks)apps/web/app/(app)/[emailAccountId]/settings/MultiAccountSection.tsx(1 hunks)apps/web/app/(app)/[emailAccountId]/usage/usage.tsx(1 hunks)apps/web/app/(app)/admin/AdminUpgradeUserForm.tsx(3 hunks)apps/web/app/(app)/admin/validation.tsx(1 hunks)apps/web/app/(app)/premium/Pricing.tsx(6 hunks)apps/web/app/(app)/premium/config.ts(7 hunks)apps/web/app/api/google/watch/all/route.ts(3 hunks)apps/web/app/api/google/webhook/process-history-item.test.ts(3 hunks)apps/web/app/api/google/webhook/process-history-item.ts(4 hunks)apps/web/app/api/google/webhook/process-history.ts(5 hunks)apps/web/app/api/google/webhook/types.ts(1 hunks)apps/web/app/api/lemon-squeezy/webhook/route.ts(9 hunks)apps/web/app/api/stripe/generate-checkout/route.ts(1 hunks)apps/web/app/api/stripe/success/route.ts(1 hunks)apps/web/app/api/stripe/webhook/route.ts(1 hunks)apps/web/app/api/user/complete-registration/route.ts(2 hunks)apps/web/app/api/user/me/route.ts(1 hunks)apps/web/components/PremiumAlert.tsx(3 hunks)apps/web/ee/LICENSE.md(1 hunks)apps/web/ee/billing/lemon/index.ts(1 hunks)apps/web/ee/billing/stripe/index.ts(1 hunks)apps/web/ee/billing/stripe/sync-stripe.ts(1 hunks)apps/web/env.ts(3 hunks)apps/web/package.json(2 hunks)apps/web/prisma/migrations/20250506025728_stripe/migration.sql(1 hunks)apps/web/prisma/schema.prisma(3 hunks)apps/web/scripts/addLemonDataToPosthog.ts(0 hunks)apps/web/scripts/encrypt-tokens.ts(0 hunks)apps/web/utils/actions/knowledge.ts(2 hunks)apps/web/utils/actions/premium.ts(9 hunks)apps/web/utils/actions/user.ts(1 hunks)apps/web/utils/config.ts(1 hunks)apps/web/utils/error.server.ts(2 hunks)apps/web/utils/middleware.ts(2 hunks)apps/web/utils/posthog.ts(2 hunks)apps/web/utils/premium/check-and-redirect-for-upgrade.tsx(1 hunks)apps/web/utils/premium/create-premium.ts(1 hunks)apps/web/utils/premium/index.ts(2 hunks)apps/web/utils/premium/server.ts(5 hunks)apps/web/utils/user/validate.ts(2 hunks)turbo.json(1 hunks)
💤 Files with no reviewable changes (3)
- apps/web/.env.example
- apps/web/scripts/encrypt-tokens.ts
- apps/web/scripts/addLemonDataToPosthog.ts
🧰 Additional context used
🧬 Code Graph Analysis (10)
apps/web/app/(app)/[emailAccountId]/usage/usage.tsx (1)
apps/web/utils/premium/index.ts (1)
isPremium(3-11)
apps/web/app/api/user/complete-registration/route.ts (1)
apps/web/utils/posthog.ts (1)
trackUserSignedUp(99-108)
apps/web/utils/error.server.ts (1)
apps/web/utils/posthog.ts (1)
trackError(132-146)
apps/web/app/api/google/webhook/process-history-item.ts (1)
apps/web/utils/premium/index.ts (1)
hasAiAccess(58-72)
apps/web/app/(app)/admin/AdminUpgradeUserForm.tsx (1)
apps/web/utils/actions/premium.ts (1)
adminChangePremiumStatusAction(249-339)
apps/web/utils/premium/check-and-redirect-for-upgrade.tsx (1)
apps/web/utils/premium/index.ts (1)
isPremium(3-11)
apps/web/components/PremiumAlert.tsx (1)
apps/web/utils/premium/index.ts (3)
isPremium(3-11)hasUnsubscribeAccess(51-56)hasAiAccess(58-72)
apps/web/app/api/google/webhook/process-history.ts (1)
apps/web/utils/premium/index.ts (1)
hasAiAccess(58-72)
apps/web/utils/premium/server.ts (5)
apps/web/utils/logger.ts (1)
createScopedLogger(17-65)apps/web/ee/billing/stripe/index.ts (1)
updateStripeSubscriptionItemQuantity(25-57)apps/web/ee/billing/lemon/index.ts (1)
updateSubscriptionItemQuantity(21-31)apps/web/utils/error.ts (1)
SafeError(83-91)apps/web/utils/premium/index.ts (2)
isPremium(3-11)hasTierAccess(74-88)
apps/web/app/api/lemon-squeezy/webhook/route.ts (3)
apps/web/utils/posthog.ts (7)
trackTrialStarted(148-157)trackUpgradedToPremium(159-168)trackSwitchedPremiumPlan(230-243)trackSubscriptionTrialStarted(170-182)trackSubscriptionCustom(184-199)trackSubscriptionCancelled(215-228)trackPaymentSuccess(245-261)apps/web/app/(app)/premium/config.ts (1)
getLemonSubscriptionTier(291-299)apps/web/utils/premium/server.ts (3)
upgradeToPremiumLemon(14-62)extendPremiumLemon(64-79)cancelPremiumLemon(81-121)
🪛 LanguageTool
apps/web/ee/LICENSE.md
[style] ~3-~3: ‘With regard to’ might be wordy. Consider a shorter alternative.
Context: ...ight (c) 2025-present Inbox Zero, Inc. With regard to the Inbox Zero Software: This software...
(EN_WORDINESS_PREMIUM_WITH_REGARD_TO)
[uncategorized] ~38-~38: If this is a compound adjective that modifies the following noun, use a hyphen.
Context: ...THER DEALINGS IN THE SOFTWARE. For all third party components incorporated into the Inbox ...
(EN_COMPOUND_ADJECTIVE_INTERNAL)
🔇 Additional comments (68)
apps/web/ee/LICENSE.md (5)
1-3: License header is clear and correctly formatted.
The title and copyright notice convey the file’s purpose unambiguously.🧰 Tools
🪛 LanguageTool
[style] ~3-~3: ‘With regard to’ might be wordy. Consider a shorter alternative.
Context: ...ight (c) 2025-present Inbox Zero, Inc. With regard to the Inbox Zero Software: This software...(EN_WORDINESS_PREMIUM_WITH_REGARD_TO)
6-15: Commercial license terms are clear.
The grant of rights, modification permissions, and subscription requirements are well articulated.
16-21: Development and testing exception is well-defined.
Allowing modification for development and testing without subscription strikes a good balance for contributors.
23-29: Clarification of AGPLv3/MIT coverage is comprehensive.
The delineation between Commercial License, AGPLv3, and MIT-covered components is precise and ensures legal clarity.
30-37: Warranty disclaimer follows standard open source practice.
The “AS IS” clause and liability limitations are appropriately stated.apps/web/app/api/user/me/route.ts (1)
23-24: Good addition of Stripe subscription dataAdding the Stripe subscription fields to the user query aligns well with the shift to Stripe for billing management. This will enable the application to make access decisions based on the unified premium tier and subscription status model.
apps/web/utils/config.ts (1)
9-10: Clear definition of knowledge base limits for basic tierThese constants provide clear numerical limits for the basic knowledge tier, which can be enforced consistently throughout the application. This supports the premium tier differentiation strategy.
apps/web/app/(app)/[emailAccountId]/automation/knowledge/KnowledgeBase.tsx (2)
30-31: Minor import reorderingThis reordering of type imports is inconsequential to functionality but maintains consistent import style.
70-70: Excellent addition of knowledge item count for premium tier enforcementPassing the current knowledge items count to the KnowledgeForm component enables the enforcement of tier-based limits, allowing the form to show appropriate upgrade prompts when users reach their plan limits.
apps/web/package.json (2)
65-65: Addition of Stripe JavaScript SDK looks good.This correctly adds the Stripe frontend SDK to support client-side Stripe functionality such as creating checkout sessions and displaying payment elements.
137-137: Addition of Stripe server library is appropriate.The server-side Stripe library is necessary for backend operations like webhook processing, subscription management, and checkout session creation. Version 18.1.0 is recent and compatible with the JavaScript SDK.
apps/web/app/(app)/[emailAccountId]/usage/usage.tsx (1)
25-28: Correctly updated isPremium call with Stripe subscription status.The update properly adds the Stripe subscription status parameter to the
isPremiumfunction call, aligning with the function's updated signature in the premium utilities. This ensures premium status is correctly determined based on both payment processors.apps/web/utils/premium/create-premium.ts (1)
3-10: LGTM: New utility function for premium record creation.This utility function cleanly abstracts the creation of a premium record for a user, connecting both the user and admin relations in a single database operation. This promotes code reuse and consistency in the premium subscription management flow.
apps/web/app/(app)/admin/AdminUpgradeUserForm.tsx (4)
8-8: Updated import to use renamed admin-specific action.The import has been correctly updated to use
adminChangePremiumStatusActioninstead of the previous action name, reflecting the refactoring of premium actions in the codebase.
20-20: Updated action reference in useAction hook.This correctly updates the action reference in the
useActionhook to match the imported action, ensuring consistency with the import change.
94-101: Addition of new Business Plus tier options.The new Business Plus tier options (monthly and annual) have been added to the dropdown options, correctly expanding the premium tier selections available to administrators. This aligns with the broader subscription model update introducing new pricing tiers.
85-122:Details
❓ Verification inconclusive
Verify that LIFETIME tier was intentionally removed.
I notice the LIFETIME tier is no longer present in the dropdown options. While this appears intentional as part of moving to Stripe subscriptions, ensure this aligns with the migration plan for existing lifetime subscribers.
🏁 Script executed:
#!/bin/bash # Check if there are any existing LIFETIME tier users in the database # and how they might be affected by this change # Check for existing LIFETIME tier usage in the Premium table echo "Checking for existing LIFETIME tier users:" rg -A 2 -B 2 "LIFETIME" --type tsLength of output: 2971
Confirm LIFETIME tier removal is intentional
I see that while you’ve removed
PremiumTier.LIFETIMEfrom the AdminUpgradeUserForm dropdown, it’s still referenced elsewhere in the codebase:
apps/web/utils/premium/index.ts(price mappings)apps/web/utils/premium/server.ts(renewal logic for LIFETIME)apps/web/utils/actions/premium.ts(upgrade action)apps/web/app/(app)/premium/config.ts(pricing, variant IDs, etc.)apps/web/app/(app)/[emailAccountId]/settings/MultiAccountSection.tsx(UI flag for lifetime users)Please verify that excluding LIFETIME here aligns with your migration/deprecation plan for existing lifetime subscribers.
- If you’re fully deprecating LIFETIME, consider removing or guarding its remaining references.
- If it should still be managed for existing users, ensure it’s handled correctly elsewhere in the UI or admin workflows.
apps/web/app/(app)/admin/validation.tsx (1)
15-16: Correctly added new premium tier optionsThe addition of
BUSINESS_PLUS_MONTHLYandBUSINESS_PLUS_ANNUALLYto the validation schema properly aligns with the introduction of Stripe billing and new premium tiers in this PR.apps/web/utils/middleware.ts (2)
60-63: Good error handling improvement for redirectsThis change correctly allows Next.js redirect errors to propagate without being caught by your general error handling. This is important for maintaining proper redirect flows in authentication and payment processes.
141-141: Simplified user ID extractionThe simplified extraction of user ID from the session object makes the code cleaner while maintaining the same functionality.
apps/web/app/api/user/complete-registration/route.ts (2)
6-6: Improved event tracking with specialized functionReplacing the generic PostHog event tracking with a specialized tracking function improves code organization and standardization.
96-96: Consistent event tracking implementationThe implementation now uses the specialized
trackUserSignedUpfunction, which standardizes event properties and naming across the application.apps/web/utils/error.server.ts (2)
2-2: Clean refactoring of error tracking dependency.The import change from
posthogCaptureEventtotrackErroraligns with the more standardized tracking approach being implemented across the codebase as part of the Stripe integration.
17-17: Good implementation of the new error tracking pattern.The function call has been updated to use the structured object parameter pattern which improves readability and maintainability. The functionality remains the same while being more consistent with modern practices.
apps/web/app/api/google/webhook/process-history-item.test.ts (4)
104-104: Good update to the test setup for the consolidated access model.The default options now properly use the new consolidated
hasAiAccessflag, aligning with the type changes.
199-200: Test correctly updated for cold email blocker check.The test for the cold email blocker feature has been properly updated to use
hasAiAccess: trueinstead of the previous access flags.
233-234: Test correctly updated for processing logic.The test for skipping further processing when a cold email is detected now properly uses the consolidated
hasAiAccessflag.
244-278:Details
✅ Verification successful
Verify that
shouldRunColdEmailBlockerimplementation is updated.The tests for
shouldRunColdEmailBlockerstill reference "hasColdEmailAccess", but the implementation function itself isn't visible in this file. Ensure that the implementation has been updated to use the new consolidated flag.
🏁 Script executed:
#!/bin/bash # Check if the shouldRunColdEmailBlocker function has been updated to use hasAiAccess rg -A 5 "shouldRunColdEmailBlocker.*=.*function" --type tsLength of output: 60
🏁 Script executed:
#!/bin/bash set -eo pipefail rg -n "shouldRunColdEmailBlocker" -C 5 --type tsLength of output: 6624
shouldRunColdEmailBlocker now uses the consolidated
hasAiAccessflagI’ve confirmed in apps/web/app/api/google/webhook/process-history-item.ts (lines 262–267) that the function signature and logic have been updated to accept
hasAiAccessas its second parameter. The existing tests still pass a boolean literal, so behavior is correct and no further changes are required.apps/web/utils/user/validate.ts (2)
24-24: Updated premium data selection to use tier-based model.The change from selecting specific feature flags to using a tier-based approach is a good design decision. This aligns with the broader refactoring to consolidate multiple feature flags into a single premium tier system.
33-35: LGTM! Updated hasAiAccess check to use tier-based permissions.The updated implementation correctly passes the user's premium tier to the
hasAiAccessfunction instead of a boolean flag, maintaining consistency with the new premium tier-based access model.apps/web/utils/premium/check-and-redirect-for-upgrade.tsx (2)
19-24: Added Stripe subscription status to premium checks.The premium data selection now includes both
lemonSqueezyRenewsAtandstripeSubscriptionStatus, properly supporting the dual payment processor approach.
31-35: LGTM! Updated isPremium check to handle both payment processors.The implementation correctly uses the updated
isPremiumfunction that handles both Lemon Squeezy renewal date and Stripe subscription status. This provides a seamless transition path for existing users while supporting the new Stripe integration.apps/web/app/api/google/watch/all/route.ts (3)
39-39: Updated premium data selection to use tier-based model.The Prisma query now selects the premium tier instead of specific feature flags, correctly aligning with the broader refactoring to a tier-based premium access model.
56-59: LGTM! Updated hasAiAccess check to use tier-based permissions.The implementation correctly passes the user's premium tier to the
hasAiAccessfunction instead of a boolean flag, maintaining consistency with the new premium tier-based access model.
61-61: Simplified access control to only require AI access.The access control logic has been streamlined to only check for AI access instead of requiring both AI and cold email access. This simplification aligns with the tier-based premium model and reduces complexity.
turbo.json (2)
59-65: Added Stripe environment variables.The addition of Stripe-related environment variables to the build task is necessary for supporting the new Stripe billing integration. The variables include the required secret keys and price IDs for different subscription tiers.
66-66: Repositioned Lemon Squeezy store ID.The
NEXT_PUBLIC_LEMON_STORE_IDvariable has been retained but moved lower in the list, correctly reflecting the dual payment processor approach with Stripe now being the primary option.apps/web/ee/billing/stripe/index.ts (4)
32-37: LGTM: Proper validation and error handling.Good implementation of input validation - the function ensures a minimum quantity of 1 and properly validates the subscription item ID with clear error messaging.
39-49: LGTM: Clean Stripe API integration.The function correctly initializes Stripe, updates the subscription item, and handles errors appropriately.
50-56: Good error logging with context.The error logging includes all relevant context (subscriptionItemId and attempted quantity) which will be helpful for debugging issues.
13-13:Details
❓ Verification inconclusive
Verify the Stripe API version is correct and available.
The
apiVersionis set to "2025-04-30.basil" which has an unusual format. Stripe API versions typically follow a YYYY-MM-DD format without any suffix.
🏁 Script executed:
#!/bin/bash # Check the validity of the Stripe API version curl -s https://api.stripe.com/v1/ \ -H "Authorization: Bearer sk_test_123" \ | grep -E "version|Version"Length of output: 103
Verify Stripe API version format
TheapiVersionvalue "2025-04-30.basil" doesn’t follow Stripe’s standard YYYY-MM-DD pattern. Please confirm this version is officially supported by checking Stripe’s API version list (https://stripe.com/docs/upgrades) and update the value if necessary.• File: apps/web/ee/billing/stripe/index.ts:13
apps/web/app/api/stripe/generate-checkout/route.ts (1)
41-44: Good practice: Using idempotency key to prevent duplicate customers.The implementation correctly uses an idempotency key with the user ID to prevent race conditions that could create duplicate Stripe customers.
apps/web/app/(app)/[emailAccountId]/automation/knowledge/KnowledgeForm.tsx (2)
47-50: LGTM: Good tier-based access check.The implementation correctly uses the
hasTierAccessutility to check if the user has access to the Business Plus or higher tier.
108-126: LGTM: Clear upgrade path for users.The UI clearly communicates the premium tier requirement and provides a direct upgrade path when limits are reached. The conditional rendering logic is correct.
apps/web/app/api/google/webhook/process-history.ts (4)
58-60: LGTM: Updated database query to include Stripe fields.The database query has been properly updated to include the new Stripe-related fields needed for premium checks.
72-77: Updated premium check to include Stripe subscription status.The code correctly updates the
isPremiumfunction call to include the Stripe subscription status in addition to the Lemon Squeezy renewal date.
93-96: LGTM: Simplified access control using unified tier-based checks.The code now uses a unified
hasAiAccessfunction based on the user's premium tier and API key, simplifying the previous separate checks for different feature flags.
165-165: LGTM: Properly passing AI access check to downstream processor.The code correctly passes the unified AI access flag to the process history function.
apps/web/app/api/google/webhook/process-history-item.ts (1)
33-39: EnsurehasAiAccessis always a non-nullable boolean
hasAiAccessis now the single gate for several AI-powered features. If any caller ofprocessHistoryItempassesundefined(e.g. a legacy call site that was not updated), TypeScript will not complain whenstrictNullChecksis off and the runtime check insideshouldRunColdEmailBlockerwill silently treat it as falsy, disabling AI features by accident.Consider:
- hasAiAccess, + hasAiAccess = false,in the destructuring list (or make the field non-optional in
ProcessHistoryOptions).
Please run a string/AST search to be sure every invocation supplies the new flag.apps/web/app/api/stripe/success/route.ts (1)
1-1: Verify thatafteris available in your target Next.js version
afterwas introduced in Next 14 and is still marked as experimental in some releases.
If the deployment environment lags behind, this import will break the route at runtime.Please double-check the Next.js version in
package.jsonor add a graceful fallback (e.g. wrap the call in a feature check) to avoid a production outage.apps/web/app/api/stripe/webhook/route.ts (1)
13-41: Strong webhook implementation with proper error handling and async processing.The webhook handler follows best practices by:
- Verifying the Stripe signature
- Processing events asynchronously with
after()- Returning a 200 response quickly to acknowledge receipt
This ensures Stripe won't retry webhooks unnecessarily while allowing your system to process them in the background.
apps/web/prisma/schema.prisma (4)
172-184: Well-structured Stripe subscription fields with comprehensive tracking.The added fields provide a complete picture of a Stripe subscription's lifecycle, including status, renewal dates, trial periods, and cancellation information. The use of nullable fields also maintains compatibility with existing records.
641-642: New premium tiers added correctly to enum.The addition of
BUSINESS_PLUS_MONTHLYandBUSINESS_PLUS_ANNUALLYtiers aligns with the changes mentioned in the AI summary about new subscription offerings.
679-681: Stripe processor type added correctly to enum.This addition enables the system to distinguish between Lemon Squeezy and Stripe payment processors in a type-safe way.
187-187:Details
✅ Verification successful
Verify if
emailAccountsAccessfield implementation is complete.This field appears to be simplified (changed from potentially multiple fields to a single field), but there's no accompanying logic shown here. Ensure that application code properly handles this change.
🏁 Script executed:
#!/bin/bash # Check for references to removed email accounts access fields rg -A 3 -B 3 "emailAccountsAccess" --glob "*.{ts,tsx,js,jsx}"Length of output: 10964
✅
emailAccountsAccessImplementation VerifiedAll usages of the unified
emailAccountsAccessfield have been updated and verified:
- Database:
apps/web/prisma/schema.prismadeclaresemailAccountsAccess Int?.- Business Logic:
apps/web/utils/actions/premium.ts(reads, compares againstuniqueEmails.length, assigns new values).apps/web/utils/premium/server.ts(field in input and passed through).- API Layer:
apps/web/app/api/user/me/route.tsincludesemailAccountsAccessin the response payload.- Admin UI:
- Validation schema in
apps/web/app/(app)/admin/validation.tsx.AdminUpgradeUserForm.tsxregisters and submitsemailAccountsAccess.- User Settings UI:
MultiAccountSection.tsxand itsMultiAccountForm/ExtraSeatsAlertcomponents consumeemailAccountsAccessto calculate and display available seats.No missing references or handling gaps detected.
apps/web/ee/billing/stripe/sync-stripe.ts (5)
8-12: Function signature is clear and focused.The function accepts a customer ID, which is the minimal required input to perform synchronization with Stripe.
16-25: Good use of Stripe API with expanded fields.The subscription list query efficiently fetches all needed data in one request by expanding related fields like payment method and price data.
28-49: Proper handling of customers without subscriptions.The code correctly handles the case where a customer has no subscriptions by clearing all subscription-related fields while maintaining the customer ID reference.
52-76: Thorough validation of subscription data structure.The code properly validates the presence and structure of subscription items, prices, and products before using them, with clear error messages when validation fails.
107-111: Error handling preserves stack trace.The catch block correctly logs the error and rethrows it to allow calling code to handle it appropriately.
apps/web/utils/actions/knowledge.ts (4)
10-16: Appropriate import additions for premium tier enforcement.The added imports bring in necessary components for error handling, configuration constants, and premium tier checking.
22-25: Updated function signature to include userId for premium checks.The function now correctly extracts userId from the context, which is necessary for the premium tier verification.
26-45: Comprehensive premium tier enforcement for knowledge creation.The implementation properly checks both the count of existing knowledge items and the content length before allowing creation, requiring the BUSINESS_PLUS_MONTHLY tier or higher for exceeding limits.
61-76: Consistent premium enforcement for knowledge updates.The update action applies the same content length check as creation, ensuring users can't bypass limits by updating existing items.
However, unlike the create action, there's no check for the total number of items here, which is appropriate since updating doesn't change the count.
apps/web/prisma/migrations/20250506025728_stripe/migration.sql (1)
13-22: Verify Postgres version or split enum additionsPostgreSQL ≤ 11 cannot add more than one enum value in a single transaction. Because the file executes two
ALTER TYPE … ADD VALUEstatements inside one migration, deployments on older clusters will fail.If you still support Postgres 11 or earlier, generate two separate migrations (one per value) or upgrade the runtime first.
apps/web/utils/actions/premium.ts (2)
440-466:after()is still experimental – guard for availability
after()fromnext/serveris only available in Next 14.2+. If your runtime is older, this import will break the bundled server code. You can fall back to a standardsetImmediate/queueMicrotaskor wrap the call:-import { after } from "next/server"; +let after: typeof import("next/server").after | ((fn: () => void) => void); +try { + // Next.js 14.2+ + after = require("next/server").after; +} catch { + after = (fn) => queueMicrotask(fn); +}Alternatively, keep the direct import but document the minimum required Next.js version.
472-478: Potential double-trial if the Price already carries a trialYou create
checkout.sessionswithsubscription_data: { trial_period_days: 7 }.
If the attached price also has a trial configured, Stripe rejects the request. Either remove the explicittrial_period_daysor assert that the chosen price has none.- subscription_data: { trial_period_days: 7 }, + // Remove if the price object already defines a trial period + subscription_data: { trial_period_days: 7 },apps/web/env.ts (1)
161-177: Unused Lemon Squeezy variant IDs could be prunedNow that the Basic and Enterprise tiers are gone, their corresponding variant IDs remain in the client schema.
Cleaning them up reduces cognitive load and makes type validation stricter.
| import { getStripe } from "@/ee/billing/stripe"; | ||
| import { env } from "@/env"; |
There was a problem hiding this comment.
💡 Verification agent
❓ Verification inconclusive
Verify if these imports are actually used in this file
The imports for getStripe and env have been added but don't appear to be used anywhere in this file. If they're not needed for this file's functionality, they should be removed to avoid confusion and maintain clean code.
🏁 Script executed:
#!/bin/bash
# Check if getStripe and env are used in this file
rg -n "getStripe\(|env\." apps/web/utils/actions/user.tsLength of output: 58
Remove unused imports in apps/web/utils/actions/user.ts
The imports getStripe and env aren’t referenced anywhere in this file. Removing them will keep the code clean and avoid confusion.
• File: apps/web/utils/actions/user.ts
Lines: 16–17
• Remove:
- import { getStripe } from "@/ee/billing/stripe";
- import { env } from "@/env";📝 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.
| import { getStripe } from "@/ee/billing/stripe"; | |
| import { env } from "@/env"; |
| hasAutomationRules: boolean; | ||
| hasColdEmailAccess: boolean; | ||
| hasAiAutomationAccess: boolean; | ||
| hasAiAccess: boolean; |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
Good simplification of access control model.
Consolidating hasColdEmailAccess and hasAiAutomationAccess into a single hasAiAccess property simplifies the permission model and reduces redundancy, which aligns well with the premium tier-based subscription approach.
🏁 Script executed:
#!/bin/bash
# Verify that all references to the old access flags have been updated
echo "Checking for any remaining references to hasColdEmailAccess and hasAiAutomationAccess"
rg -l "hasColdEmailAccess|hasAiAutomationAccess" --type tsLength of output: 302
Action Required: Replace Legacy Access Flags in Tests
The grep check uncovered a leftover reference to hasColdEmailAccess/hasAiAutomationAccess in your tests. Please update this to use the new hasAiAccess property.
• apps/web/app/api/google/webhook/process-history-item.test.ts
– Replace any occurrences of hasColdEmailAccess or hasAiAutomationAccess with hasAiAccess and adjust expected values accordingly.
| <Button type="submit" loading={isExecuting}> | ||
| Save | ||
| </Button> |
There was a problem hiding this comment.
🛠️ Refactor suggestion
UI simplified for the Stripe integration.
The UI has been streamlined by removing the Lemon Squeezy payment link button and keeping only the Save button. This aligns with the migration to Stripe for subscription management.
However, there's no clear alternative action for users who need to purchase more seats. Consider adding guidance for users when needsToPurchaseMoreSeats is true:
- <Button type="submit" loading={isExecuting}>
- Save
- </Button>
+ {needsToPurchaseMoreSeats ? (
+ <>
+ <Button type="submit" loading={isExecuting} disabled>
+ Save
+ </Button>
+ <p className="mt-2 text-sm text-muted-foreground">
+ You need to purchase additional seats before saving. Visit the billing section to upgrade.
+ </p>
+ </>
+ ) : (
+ <Button type="submit" loading={isExecuting}>
+ Save
+ </Button>
+ )}📝 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.
| <Button type="submit" loading={isExecuting}> | |
| Save | |
| </Button> | |
| {needsToPurchaseMoreSeats ? ( | |
| <> | |
| <Button type="submit" loading={isExecuting} disabled> | |
| Save | |
| </Button> | |
| <p className="mt-2 text-sm text-muted-foreground"> | |
| You need to purchase additional seats before saving. Visit the billing section to upgrade. | |
| </p> | |
| </> | |
| ) : ( | |
| <Button type="submit" loading={isExecuting}> | |
| Save | |
| </Button> | |
| )} |
|
|
||
| const STRIPE_PRICE_ID_CONFIG: Record< | ||
| PremiumTier, | ||
| { | ||
| // active price id | ||
| priceId?: string; | ||
| // Allow handling of old price ids | ||
| oldPriceIds?: string[]; | ||
| } | ||
| > = { | ||
| [PremiumTier.BASIC_MONTHLY]: {}, | ||
| [PremiumTier.BASIC_ANNUALLY]: {}, | ||
| [PremiumTier.PRO_MONTHLY]: {}, | ||
| [PremiumTier.PRO_ANNUALLY]: {}, | ||
| [PremiumTier.BUSINESS_MONTHLY]: { | ||
| priceId: env.NEXT_PUBLIC_STRIPE_BUSINESS_MONTHLY_PRICE_ID, | ||
| }, | ||
| [PremiumTier.BUSINESS_ANNUALLY]: { | ||
| priceId: env.NEXT_PUBLIC_STRIPE_BUSINESS_ANNUALLY_PRICE_ID, | ||
| }, | ||
| [PremiumTier.BUSINESS_PLUS_MONTHLY]: { | ||
| priceId: env.NEXT_PUBLIC_STRIPE_BUSINESS_PLUS_MONTHLY_PRICE_ID, | ||
| }, | ||
| [PremiumTier.BUSINESS_PLUS_ANNUALLY]: { | ||
| priceId: env.NEXT_PUBLIC_STRIPE_BUSINESS_PLUS_ANNUALLY_PRICE_ID, | ||
| }, | ||
| [PremiumTier.COPILOT_MONTHLY]: {}, | ||
| [PremiumTier.LIFETIME]: {}, | ||
| }; |
There was a problem hiding this comment.
Fail-fast when Stripe price IDs are missing
STRIPE_PRICE_ID_CONFIG silently stores undefined when the env variable is absent.
Down-stream code then returns null, which surfaces only at checkout time.
Recommend validating at start-up:
priceId: env.NEXT_PUBLIC_STRIPE_BUSINESS_MONTHLY_PRICE_ID
- ,
+ ?? (() => { throw new Error("Missing STRIPE_BUSINESS_MONTHLY_PRICE_ID"); })(),or run a dedicated assertion in getStripePriceId.
📝 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.
| const STRIPE_PRICE_ID_CONFIG: Record< | |
| PremiumTier, | |
| { | |
| // active price id | |
| priceId?: string; | |
| // Allow handling of old price ids | |
| oldPriceIds?: string[]; | |
| } | |
| > = { | |
| [PremiumTier.BASIC_MONTHLY]: {}, | |
| [PremiumTier.BASIC_ANNUALLY]: {}, | |
| [PremiumTier.PRO_MONTHLY]: {}, | |
| [PremiumTier.PRO_ANNUALLY]: {}, | |
| [PremiumTier.BUSINESS_MONTHLY]: { | |
| priceId: env.NEXT_PUBLIC_STRIPE_BUSINESS_MONTHLY_PRICE_ID, | |
| }, | |
| [PremiumTier.BUSINESS_ANNUALLY]: { | |
| priceId: env.NEXT_PUBLIC_STRIPE_BUSINESS_ANNUALLY_PRICE_ID, | |
| }, | |
| [PremiumTier.BUSINESS_PLUS_MONTHLY]: { | |
| priceId: env.NEXT_PUBLIC_STRIPE_BUSINESS_PLUS_MONTHLY_PRICE_ID, | |
| }, | |
| [PremiumTier.BUSINESS_PLUS_ANNUALLY]: { | |
| priceId: env.NEXT_PUBLIC_STRIPE_BUSINESS_PLUS_ANNUALLY_PRICE_ID, | |
| }, | |
| [PremiumTier.COPILOT_MONTHLY]: {}, | |
| [PremiumTier.LIFETIME]: {}, | |
| }; | |
| const STRIPE_PRICE_ID_CONFIG: Record< | |
| PremiumTier, | |
| { | |
| // active price id | |
| priceId?: string; | |
| // Allow handling of old price ids | |
| oldPriceIds?: string[]; | |
| } | |
| > = { | |
| [PremiumTier.BASIC_MONTHLY]: {}, | |
| [PremiumTier.BASIC_ANNUALLY]: {}, | |
| [PremiumTier.PRO_MONTHLY]: {}, | |
| [PremiumTier.PRO_ANNUALLY]: {}, | |
| [PremiumTier.BUSINESS_MONTHLY]: { | |
| - priceId: env.NEXT_PUBLIC_STRIPE_BUSINESS_MONTHLY_PRICE_ID, | |
| + priceId: env.NEXT_PUBLIC_STRIPE_BUSINESS_MONTHLY_PRICE_ID | |
| + ?? (() => { throw new Error("Missing STRIPE_BUSINESS_MONTHLY_PRICE_ID"); })(), | |
| }, | |
| [PremiumTier.BUSINESS_ANNUALLY]: { | |
| priceId: env.NEXT_PUBLIC_STRIPE_BUSINESS_ANNUALLY_PRICE_ID, | |
| }, | |
| [PremiumTier.BUSINESS_PLUS_MONTHLY]: { | |
| priceId: env.NEXT_PUBLIC_STRIPE_BUSINESS_PLUS_MONTHLY_PRICE_ID, | |
| }, | |
| [PremiumTier.BUSINESS_PLUS_ANNUALLY]: { | |
| priceId: env.NEXT_PUBLIC_STRIPE_BUSINESS_PLUS_ANNUALLY_PRICE_ID, | |
| }, | |
| [PremiumTier.COPILOT_MONTHLY]: {}, | |
| [PremiumTier.LIFETIME]: {}, | |
| }; |
| if ( | ||
| ["subscription_payment_success", "order_created"].includes( | ||
| payload.meta.event_name, | ||
| ) | ||
| ) { | ||
| return NextResponse.json({ ok: true }); |
There was a problem hiding this comment.
subscription_payment_success is ignored and later (unreachable) handled – remove it from the ignore list or delete the dead-code block.
Lines 42-47 short-circuit the request when the event name equals "subscription_payment_success", yet lines 96-99 attempt to process that same event.
Because the early return always wins, the code in 96-99 will never run and payment successes are silently dropped from analytics and DB updates.
- if (
- ["subscription_payment_success", "order_created"].includes(
- payload.meta.event_name,
- )
- ) {
+ // Ignore events we truly don't care about
+ if (
+ ["order_created"].includes(
+ payload.meta.event_name,
+ )
+ ) {Remove the event from either place (or delete the second branch) so that one clear path remains.
Also applies to: 96-99
| export const isPremium = ( | ||
| lemonSqueezyRenewsAt: Date | null, | ||
| stripeSubscriptionStatus: string | null, | ||
| ): boolean => { | ||
| if (lemonSqueezyRenewsAt) return new Date(lemonSqueezyRenewsAt) > new Date(); | ||
| if (stripeSubscriptionStatus) return stripeSubscriptionStatus === "active"; | ||
|
|
||
| // if renewsAt is less than 6 months in the future then it's a monthly plan | ||
| return PremiumTier.BUSINESS_MONTHLY; | ||
| return false; | ||
| }; |
There was a problem hiding this comment.
isPremium ignores an active Stripe subscription when Lemon renewal date exists but is expired.
If a user migrated from Lemon to Stripe, lemonSqueezyRenewsAt will be set (historical) yet in the past.
The current early return prevents the Stripe check from running, yielding a false negative.
- if (lemonSqueezyRenewsAt) return new Date(lemonSqueezyRenewsAt) > new Date();
- if (stripeSubscriptionStatus) return stripeSubscriptionStatus === "active";
+ // Lemon Squeezy takes precedence only if it is still active
+ if (
+ lemonSqueezyRenewsAt &&
+ new Date(lemonSqueezyRenewsAt) > new Date()
+ )
+ return true;
+
+ if (stripeSubscriptionStatus)
+ return stripeSubscriptionStatus === "active";This guarantees the Stripe status is considered whenever Lemon has lapsed.
📝 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.
| export const isPremium = ( | |
| lemonSqueezyRenewsAt: Date | null, | |
| stripeSubscriptionStatus: string | null, | |
| ): boolean => { | |
| if (lemonSqueezyRenewsAt) return new Date(lemonSqueezyRenewsAt) > new Date(); | |
| if (stripeSubscriptionStatus) return stripeSubscriptionStatus === "active"; | |
| // if renewsAt is less than 6 months in the future then it's a monthly plan | |
| return PremiumTier.BUSINESS_MONTHLY; | |
| return false; | |
| }; | |
| export const isPremium = ( | |
| lemonSqueezyRenewsAt: Date | null, | |
| stripeSubscriptionStatus: string | null, | |
| ): boolean => { | |
| // Lemon Squeezy takes precedence only if it is still active | |
| if ( | |
| lemonSqueezyRenewsAt && | |
| new Date(lemonSqueezyRenewsAt) > new Date() | |
| ) | |
| return true; | |
| if (stripeSubscriptionStatus) | |
| return stripeSubscriptionStatus === "active"; | |
| return false; | |
| }; |
There was a problem hiding this comment.
Actionable comments posted: 3
♻️ Duplicate comments (4)
apps/web/utils/premium/index.ts (1)
3-14:⚠️ Potential issue
isPremiumignores an active Stripe subscription when Lemon renewal date exists but is expired.If a user migrated from Lemon to Stripe, the current implementation doesn't check Stripe status when Lemon is expired but still present.
- if (lemonSqueezyRenewsAt) return new Date(lemonSqueezyRenewsAt) > new Date(); + // Lemon Squeezy takes precedence only if it is still active + if (lemonSqueezyRenewsAt && new Date(lemonSqueezyRenewsAt) > new Date()) return true;apps/web/utils/posthog.ts (1)
263-265: Fix potential PII leakage in thetrackStripeEventfunctionThis implementation passes the raw data object to PostHog, which could lead to oversized payloads and PII leakage if it contains a full Stripe.Event. Stripe events can contain sensitive data like emails, addresses, and payment details.
I suggest extracting only the necessary, non-sensitive fields:
-export async function trackStripeEvent(email: string, data: any) { - return posthogCaptureEvent(email, "Stripe event", data); +export async function trackStripeEvent( + email: string, + event: Stripe.Event, +) { + const { id, type, api_version } = event; + return posthogCaptureEvent(email, "Stripe event", { + id, + type, + api_version, + object: event.data?.object?.object, // e.g. 'invoice' + }); }apps/web/app/(app)/premium/Pricing.tsx (2)
298-305: Addaria-disabledfor accessibility parityYou already disable the CTA while it’s loading—great! Adding
aria-disabledmakes the state obvious to assistive technologies:- disabled={loading} + disabled={loading} + aria-disabled={loading}
30-33:⚠️ Potential issueAnnual price still shows “/month” – update suffix
The label for the annual frequency still renders “/month”, which is misleading and was pointed out in the earlier review.
- { value: "annually" as const, label: "Annually", priceSuffix: "/month" }, + { value: "annually" as const, label: "Annually", priceSuffix: "/year" },
🧹 Nitpick comments (4)
apps/web/utils/premium/index.ts (1)
79-93: Variable naming inconsistency inhasTierAccess.The function returns a boolean indicating whether the user has access to a particular tier, but the variable storing the result is named
hasAiAccesswhich is confusing since this function is for general tier access.- const hasAiAccess = ranking >= tierRanking[minimumTier]; + const hasTierAccess = ranking >= tierRanking[minimumTier]; - return hasAiAccess; + return hasTierAccess;apps/web/utils/posthog.ts (2)
148-182: Merge duplicate trial tracking functionsThe
trackTrialStartedandtrackSubscriptionTrialStartedfunctions are nearly identical with only a slight difference in the event name. The properties being set are exactly the same.Consider consolidating these into a single function with a parameter to specify the event name or context:
-export async function trackTrialStarted(email: string, attributes: any) { - return posthogCaptureEvent(email, "Premium trial started", { - ...attributes, - $set: { - premium: true, - premiumTier: "subscription", - premiumStatus: "on_trial", - }, - }); -} - -export async function trackSubscriptionTrialStarted( - email: string, - attributes: any, -) { - return posthogCaptureEvent(email, "Premium subscription trial started", { - ...attributes, - $set: { - premium: true, - premiumTier: "subscription", - premiumStatus: "on_trial", - }, - }); -} +export async function trackTrialStarted( + email: string, + attributes: any, + context?: 'subscription' +) { + const eventName = context + ? `Premium ${context} trial started` + : "Premium trial started"; + + return posthogCaptureEvent(email, eventName, { + ...attributes, + $set: { + premium: true, + premiumTier: "subscription", + premiumStatus: "on_trial", + }, + }); +}
132-146: Improve type safety with more specific typesMost tracking functions use
anyfor theattributesparameter, which reduces type safety. This could lead to mistakes when using these functions elsewhere in the codebase.For example in the
trackErrorfunction, you've properly defined a specific interface. Consider doing the same for other functions:+interface SubscriptionAttributes { + status?: string; + planId?: string; + price?: number; + interval?: string; + [key: string]: any; +} export async function trackSubscriptionCustom( email: string, status: string, - attributes: any, + attributes: SubscriptionAttributes, ) { // ... } export async function trackSubscriptionStatusChanged( email: string, - attributes: any, + attributes: SubscriptionAttributes & { status: string }, ) { // ... }Also applies to: 184-228
apps/web/app/(app)/premium/Pricing.tsx (1)
191-194: Consider computing the “Save up to …%” badge dynamicallyThe badge is hard-coded to “20%”, which may drift from the values in
tier.discount. Rendering the maximum discount from the config avoids future mismatches and maintenance overhead.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (7)
apps/web/app/(app)/premium/Pricing.tsx(6 hunks)apps/web/app/api/google/watch/all/route.ts(4 hunks)apps/web/app/api/google/webhook/process-history.ts(6 hunks)apps/web/app/api/resend/summary/all/route.ts(1 hunks)apps/web/app/api/stripe/webhook/route.ts(1 hunks)apps/web/utils/posthog.ts(2 hunks)apps/web/utils/premium/index.ts(2 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
- apps/web/app/api/google/webhook/process-history.ts
- apps/web/app/api/google/watch/all/route.ts
- apps/web/app/api/stripe/webhook/route.ts
🔇 Additional comments (3)
apps/web/utils/premium/index.ts (2)
41-52: Verify the LIFETIME tier status in the ranking system.According to the PR summary, the lifetime tier was removed, yet it still appears in the tier ranking with the highest value (10).
Please confirm if the LIFETIME tier should still be included in the ranking system or if this is an oversight.
54-61: ImprovedhasUnsubscribeAccessimplementation addresses previous issues.The function now properly checks for tier existence and positive unsubscribe credits, fixing the previous issue with null credits.
However, the credit check could be slightly more explicit:
- if (unsubscribeCredits && unsubscribeCredits > 0) return true; + if (typeof unsubscribeCredits === "number" && unsubscribeCredits > 0) return true;This makes the check more explicit about the type expectation.
apps/web/app/api/resend/summary/all/route.ts (1)
31-37: Premium user filtering correctly updated to include Stripe subscriptions.The query now properly identifies premium users from both payment processors, aligning with the updated
isPremiumfunction logic. The included statuses ("active" and "trialing") match the accepted premium statuses.
| export async function trackPaymentSuccess({ | ||
| email, | ||
| totalPaidUSD, | ||
| lemonSqueezyId, | ||
| lemonSqueezyType, | ||
| }: { | ||
| email: string; | ||
| totalPaidUSD: number | undefined; | ||
| lemonSqueezyId: string; | ||
| lemonSqueezyType: string; | ||
| }) { | ||
| return posthogCaptureEvent(email, "Payment success", { | ||
| totalPaidUSD, | ||
| lemonSqueezyId, | ||
| lemonSqueezyType, | ||
| }); | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Update payment success tracking for Stripe integration
The trackPaymentSuccess function only contains Lemon Squeezy-specific parameters, but this PR is integrating Stripe for payments.
Consider generalizing this function to handle both payment processors:
export async function trackPaymentSuccess({
email,
totalPaidUSD,
- lemonSqueezyId,
- lemonSqueezyType,
+ paymentId,
+ paymentProcessor = "stripe",
+ paymentType,
}: {
email: string;
totalPaidUSD: number | undefined;
- lemonSqueezyId: string;
- lemonSqueezyType: string;
+ paymentId: string;
+ paymentProcessor?: "stripe" | "lemonSqueezy";
+ paymentType: string;
}) {
return posthogCaptureEvent(email, "Payment success", {
totalPaidUSD,
- lemonSqueezyId,
- lemonSqueezyType,
+ paymentId,
+ paymentProcessor,
+ paymentType,
});
}📝 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.
| export async function trackPaymentSuccess({ | |
| email, | |
| totalPaidUSD, | |
| lemonSqueezyId, | |
| lemonSqueezyType, | |
| }: { | |
| email: string; | |
| totalPaidUSD: number | undefined; | |
| lemonSqueezyId: string; | |
| lemonSqueezyType: string; | |
| }) { | |
| return posthogCaptureEvent(email, "Payment success", { | |
| totalPaidUSD, | |
| lemonSqueezyId, | |
| lemonSqueezyType, | |
| }); | |
| } | |
| export async function trackPaymentSuccess({ | |
| email, | |
| totalPaidUSD, | |
| paymentId, | |
| paymentProcessor = "stripe", | |
| paymentType, | |
| }: { | |
| email: string; | |
| totalPaidUSD: number | undefined; | |
| paymentId: string; | |
| paymentProcessor?: "stripe" | "lemonSqueezy"; | |
| paymentType: string; | |
| }) { | |
| return posthogCaptureEvent(email, "Payment success", { | |
| totalPaidUSD, | |
| paymentId, | |
| paymentProcessor, | |
| paymentType, | |
| }); | |
| } |
| export async function trackStripeCheckoutCreated(email: string) { | ||
| return posthogCaptureEvent(email, "Stripe checkout created"); | ||
| } | ||
|
|
||
| export async function trackStripeCheckoutCompleted(email: string) { | ||
| return posthogCaptureEvent(email, "Stripe checkout completed"); | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Add missing checkout details to checkout event tracking
The trackStripeCheckoutCreated and trackStripeCheckoutCompleted functions don't capture any details about the checkout session, making it difficult to correlate these events with specific transactions.
Consider adding session ID and other relevant non-sensitive information:
-export async function trackStripeCheckoutCreated(email: string) {
- return posthogCaptureEvent(email, "Stripe checkout created");
+export async function trackStripeCheckoutCreated(
+ email: string,
+ sessionId: string,
+ priceId?: string
+) {
+ return posthogCaptureEvent(email, "Stripe checkout created", {
+ sessionId,
+ priceId,
+ });
}
-export async function trackStripeCheckoutCompleted(email: string) {
- return posthogCaptureEvent(email, "Stripe checkout completed");
+export async function trackStripeCheckoutCompleted(
+ email: string,
+ sessionId: string
+) {
+ return posthogCaptureEvent(email, "Stripe checkout completed", {
+ sessionId,
+ });
}📝 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.
| export async function trackStripeCheckoutCreated(email: string) { | |
| return posthogCaptureEvent(email, "Stripe checkout created"); | |
| } | |
| export async function trackStripeCheckoutCompleted(email: string) { | |
| return posthogCaptureEvent(email, "Stripe checkout completed"); | |
| } | |
| export async function trackStripeCheckoutCreated( | |
| email: string, | |
| sessionId: string, | |
| priceId?: string | |
| ) { | |
| return posthogCaptureEvent(email, "Stripe checkout created", { | |
| sessionId, | |
| priceId, | |
| }); | |
| } | |
| export async function trackStripeCheckoutCompleted( | |
| email: string, | |
| sessionId: string | |
| ) { | |
| return posthogCaptureEvent(email, "Stripe checkout completed", { | |
| sessionId, | |
| }); | |
| } |
Summary by CodeRabbit
New Features
Improvements
Bug Fixes
Chores