Skip to content

Stripe#425

Merged
elie222 merged 35 commits intomainfrom
stripe
May 8, 2025
Merged

Stripe#425
elie222 merged 35 commits intomainfrom
stripe

Conversation

@elie222
Copy link
Owner

@elie222 elie222 commented May 8, 2025

Summary by CodeRabbit

  • New Features

    • Introduced Stripe integration for subscription management, including checkout, billing portal, and webhook support.
    • Added new premium tiers: Business Plus Monthly and Annually.
    • Enhanced knowledge base limits and upgrade prompts based on subscription tier.
    • Added license file for Inbox Zero Commercial License.
  • Improvements

    • Unified premium access logic and tier enforcement across the app.
    • Refactored premium plan management UI for clarity and modularity.
    • Updated pricing tiers and descriptions.
    • Simplified environment variable usage and configuration.
  • Bug Fixes

    • Improved error handling and analytics tracking for subscription and payment events.
  • Chores

    • Removed legacy Lemon Squeezy payment links and scripts.
    • Updated database schema to support Stripe and new premium tiers.
    • Added Stripe dependencies and updated environment variables.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented May 8, 2025

## 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 page

Possibly related PRs

  • elie222/inbox-zero#362: Refactored pricing and premium tier configurations, including changes to the pricing UI and tier definitions, directly related to the current PR's premium model overhaul.
  • elie222/inbox-zero#284: Added and implemented Lemon Squeezy plan switching, which is now commented out or removed in the current PR, making them directly related but opposite in effect.

Poem

Stripe and Lemon, side by side,
The billing bunnies now can glide.
Tiers refined and logic neat,
Old scripts gone, new flows complete!
With carrots (err, credits) in a row,
Premium plans are set to grow.
🐇💳✨


<!-- walkthrough_end -->

<!-- announcements_start -->

> [!TIP]
> <details>
> <summary>⚡️ Faster reviews with caching</summary>
> 
> - CodeRabbit now supports caching for code and dependencies, helping speed up reviews. This means quicker feedback, reduced wait times, and a smoother review experience overall. Cached data is encrypted and stored securely. This feature will be automatically enabled for all accounts on May 16th. To opt out, configure `Review - Disable Cache` at either the organization or repository level. If you prefer to disable all data retention across your organization, simply turn off the `Data Retention` setting under your Organization Settings.
> 
> Enjoy the performance boost—your workflow just got faster.
> 
> </details>

<!-- announcements_end -->
<!-- internal state start -->


<!-- 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 -->

<!-- internal state end -->
<!-- finishing_touch_checkbox_start -->

<details open="true">
<summary>✨ Finishing Touches</summary>

- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> 📝 Generate Docstrings

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---

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

<details>
<summary>❤️ Share</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)
- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)

</details>

<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=elie222/inbox-zero&utm_content=425):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Generate unit testing code for this file.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai generate unit testing code for this file.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and generate unit testing code.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

### Support

Need help? Create a ticket on our [support page](https://www.coderabbit.ai/contact-us/support) for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai generate sequence diagram` to generate a sequence diagram of the changes in this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->

@vercel
Copy link

vercel bot commented May 8, 2025

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Updated (UTC)
inbox-zero ✅ Ready (Inspect) Visit Preview May 8, 2025 4:36pm

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

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, and coldEmailBlockerAccess) 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 ts

Length 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 uses stripeSubscriptionStatus (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 shouldRunColdEmailBlocker function still accepts a parameter named hasColdEmailAccess while the rest of the codebase has been updated to use hasAiAccess.

-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 hasAiAccess parameter 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 hasAiAccess instead of hasColdEmailAccess

The implementation of shouldRunColdEmailBlocker now expects a hasAiAccess boolean parameter, but the tests still refer to and name it hasColdEmailAccess. 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 the shouldRunColdEmailBlocker calls:

  • Change “hasColdEmailAccess” → “hasAiAccess” in the it titles
  • Rename the variable passed (and any inline comments) from hasColdEmailAccess to hasAiAccess

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

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. Parallel Processing:

    • Consider running both old and new processors in parallel for a short period.
    • This helps catch any issues before fully switching.
  7. Fallback and Rollback Plan:

    • Prepare a rollback plan in case the migration encounters critical problems.
    • Ensure data consistency and availability for customers during rollback.
  8. 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.
  9. Monitor and Support:

    • Monitor all transactions closely immediately after migration.
    • Provide dedicated customer support to handle any subscription or payment issues quickly.
  10. 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 issue

Multi-account seat upsell only works for Lemon Squeezy

When the premium row was created through Stripe, lemonSqueezySubscriptionItemId will be null, 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 updateStripeSubscriptionItemQuantity call.

apps/web/utils/posthog.ts (2)

8-41: 🛠️ Refactor suggestion

Handle missing env vars and HTTP errors in getPosthogUserId

getPosthogUserId assumes the PostHog env variables are present and that the fetch call succeeds.
If either assumption breaks, the function throws at responseGet.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 suggestion

Avoid creating a new PostHog client on every capture

Instantiating 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 the PostHog client 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 detected

Two 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 comments

These 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 duplication

The premium tier check logic is duplicated between createKnowledgeAction and updateKnowledgeAction. 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 with after() or return the Promise

The 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 handling
apps/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 present

If both stripeSubscriptionItemId and lemonSqueezySubscriptionItemId are null, the function silently exits and the caller has no clue why seats were not synced.
Consider adding an else branch with a logger.warn so ops can spot mis-configured premium rows.

apps/web/utils/actions/premium.ts (2)

59-63: isPremium again ignores Stripe trials

Same 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 a cancel_url

Without 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 clarity

The ternary mixes two very different calls. Using explicit if/else makes 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

extendPremiumLemon updates 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: hasBothStripeAndLemon naming 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 to PremiumTier.BUSINESS_*.
businessPlusTier.name = "Business" while it maps to PremiumTier.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: tierToVariantId includes null values – handle when serialising

Functions consuming tierToVariantId should expect null and avoid inserting null into 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=null bugs.


34-44: Spelling: pricingAdditonalEmailpricingAdditionalEmail

The misspelling propagates throughout the file. Renaming now will spare future typos.

-export const pricingAdditonalEmail
+export const pricingAdditionalEmail

Remember to update all usages.

apps/web/utils/premium/index.ts (1)

58-69: Nit: variable shadowing & clarity in hasAiAccess

The local variable hasAiAccess duplicates the function name and represents a boolean unrelated to AI per-se (it’s a general tier comparison). Consider renaming to hasRequiredTier (or similar) to improve readability and avoid future confusion.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bbe909b and 44f0a43.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is 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 added

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


🏁 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.json

Length 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-js to 7.3.0 (apps/web/package.json:65)
  • stripe to 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 billing

The code now passes both Lemon Squeezy renewal date and Stripe subscription status to the isPremium function, 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 functionality

Commenting out the switchPremiumPlan function 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 action

The code now uses adminChangePremiumStatusAction which is specifically designed for admin-level operations, improving role separation and security.


20-20: Action implementation consistently updated

This line correctly updates the action being executed to match the imported admin-specific action.


94-101: New premium tiers correctly added

The 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 function

The import is now using the more specialized trackError function from PostHog utilities, which is part of the broader refactoring of event tracking across the application.


17-17: Improved error tracking with consolidated parameters

The implementation now uses the more specialized trackError function 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 tiers

The 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 creation

This 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 redirects

This 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 extraction

Good 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 property

This 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 trackUserSignedUp function 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 hasColdEmailAccess import as part of the consolidation of premium access checks.


39-40: Streamlined premium data selection.

Now only retrieving the tier field 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 hasAiAccess instead 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 (hasColdEmailAccess and hasAiAutomationAccess) to a unified hasAiAccess flag 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 hasAiAccess flag 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 aiAutomationAccess to tier, aligning with the new tier-based access control system.


33-36:

Details

✅ Verification successful

Correctly updated hasAiAccess call with tier-based parameter.

The hasAiAccess call now uses the premium tier instead of the specific feature flag, which supports the consolidated access control model.

Let me verify that the hasAiAccess function 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 hasAiAccess definition 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 ts

Length of output: 398


hasAiAccess call updated correctly

The hasAiAccess helper 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.aiApiKey

This 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 hasColdEmailAccess import 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 hasUnsubscribeAccess and hasAiAccess utility 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 knowledgeItemsCount prop 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 hasTierAccess utility with the BUSINESS_PLUS_MONTHLY minimum 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 stripeSubscriptionStatus and tier, replacing legacy feature flags and aligning with the new subscription model.


72-74: Dual payment processor support in premium check.

The isPremium function 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 hasAiAccess check 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 single hasAiAccess parameter, streamlining the interface.


138-138: Updated cold email blocker check.

The shouldRunColdEmailBlocker function call now correctly uses the unified hasAiAccess flag 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 hasAiAccess flag, making the code more readable and consistent with the new access control model.


262-265: Simplified cold email blocker function signature.

The shouldRunColdEmailBlocker function has been updated to use the single hasAiAccess parameter, 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 mechanisms

The 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 of after() for non-blocking analytics

Using 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 items

The 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 of after() helper

after was 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: Keep experimental__runtimeEnv list in sync with client schema

You 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 passing tier to 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 deprecated LIFETIME tier in tierRanking

The PR description states that the LIFETIME tier 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.

Comment on lines +213 to +215
<Button type="submit" loading={isExecuting}>
Save
</Button>
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 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=ts

Length 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 onSubmit callback (apps/web/app/(app)/[emailAccountId]/settings/MultiAccountSection.tsx lines 168–174), submission simply returns if needsToPurchaseMoreSeats is true.
  • The ExtraSeatsAlert component (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.

Comment on lines 263 to 265
export async function trackStripeEvent(email: string, event: Stripe.Event) {
return posthogCaptureEvent(email, "Stripe event", event);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

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.

Suggested change
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'
});
}

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 15

🔭 Outside diff range comments (2)
apps/web/utils/premium/server.ts (1)

107-113: ⚠️ Potential issue

Removed columns still referenced – this will crash after migration

bulkUnsubscribeAccess, aiAutomationAccess, and coldEmailBlockerAccess were dropped from the Premium table. Updating them here will raise PrismaClientKnownRequestError (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 update
apps/web/utils/posthog.ts (1)

74-97: 🛠️ Refactor suggestion

Creating a new PostHog client per call is wasteful and can throttle event delivery.

posthogCaptureEvent instantiates 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 code

This 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 checks

The three explicit comparisons are correct but verbose and easy to forget when a new ColdEmailSetting is 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: isUserPremium can be simplified

isPremium already folds null handling; the extra premium && … 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

syncStripeDataToDb may hit Stripe’s API, which can add hundreds of ms to the response.
You’re already using after; 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.allSettled correctly 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_end is being read from subscriptionItem but 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 end

Double-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, and stripeSubscriptionItemId are all nullable. Postgres allows multiple NULL rows 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

isPremium only treats "active" as premium. Stripe marks new subscriptions "trialing" by default (you create them with a 7-day trial in generateCheckoutSessionAction). During that period checkHasAccess will deny access even though the user has an active subscription.

Extend isPremium to 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 causing window.opener leaks.
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-coded

The badge is static even though the discount percentages are calculated per tier.
Consider deriving the figure dynamically from tiers to avoid future drift when marketing changes.


298-354: Race-condition / double-click protection

There is no guard against a user double-clicking the CTA while loading is already true.
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: userPremiumTier may be null

getCTAText() accesses userPremiumTier without 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_MONTHLY relies 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 return undefined.

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 Plus

Most 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 & runtimeEnv

Every 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 trackTrialStarted and trackUpgradedToPremium relies 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 status once (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.

trackStripeEvent forwards the entire Stripe.Event object. 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: hasAiAccess relies on duplicated constants – could be simplified with hasTierAccess.

hasAiAccess repeats ranking comparisons already abstracted by hasTierAccess.

-  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

📥 Commits

Reviewing files that changed from the base of the PR and between bbe909b and 44f0a43.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is 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 data

Adding 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 tier

These 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 reordering

This reordering of type imports is inconsequential to functionality but maintains consistent import style.


70-70: Excellent addition of knowledge item count for premium tier enforcement

Passing 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 isPremium function 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 adminChangePremiumStatusAction instead 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 useAction hook 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 ts

Length of output: 2971


Confirm LIFETIME tier removal is intentional

I see that while you’ve removed PremiumTier.LIFETIME from 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 options

The addition of BUSINESS_PLUS_MONTHLY and BUSINESS_PLUS_ANNUALLY to 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 redirects

This 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 extraction

The 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 function

Replacing the generic PostHog event tracking with a specialized tracking function improves code organization and standardization.


96-96: Consistent event tracking implementation

The implementation now uses the specialized trackUserSignedUp function, 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 posthogCaptureEvent to trackError aligns 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 hasAiAccess flag, 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: true instead 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 hasAiAccess flag.


244-278:

Details

✅ Verification successful

Verify that shouldRunColdEmailBlocker implementation is updated.

The tests for shouldRunColdEmailBlocker still 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 ts

Length of output: 60


🏁 Script executed:

#!/bin/bash
set -eo pipefail
rg -n "shouldRunColdEmailBlocker" -C 5 --type ts

Length of output: 6624


shouldRunColdEmailBlocker now uses the consolidated hasAiAccess flag

I’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 hasAiAccess as 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 hasAiAccess function 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 lemonSqueezyRenewsAt and stripeSubscriptionStatus, properly supporting the dual payment processor approach.


31-35: LGTM! Updated isPremium check to handle both payment processors.

The implementation correctly uses the updated isPremium function 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 hasAiAccess function 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_ID variable 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 apiVersion is 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
The apiVersion value "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 hasTierAccess utility 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 isPremium function 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 hasAiAccess function 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: Ensure hasAiAccess is always a non-nullable boolean

hasAiAccess is now the single gate for several AI-powered features. If any caller of processHistoryItem passes undefined (e.g. a legacy call site that was not updated), TypeScript will not complain when strictNullChecks is off and the runtime check inside shouldRunColdEmailBlocker will 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 that after is available in your target Next.js version

after was 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.json or 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:

  1. Verifying the Stripe signature
  2. Processing events asynchronously with after()
  3. 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_MONTHLY and BUSINESS_PLUS_ANNUALLY tiers 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 emailAccountsAccess field 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


emailAccountsAccess Implementation Verified

All usages of the unified emailAccountsAccess field have been updated and verified:

  • Database: apps/web/prisma/schema.prisma declares emailAccountsAccess Int?.
  • Business Logic:
    • apps/web/utils/actions/premium.ts (reads, compares against uniqueEmails.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.ts includes emailAccountsAccess in the response payload.
  • Admin UI:
    • Validation schema in apps/web/app/(app)/admin/validation.tsx.
    • AdminUpgradeUserForm.tsx registers and submits emailAccountsAccess.
  • User Settings UI:
    • MultiAccountSection.tsx and its MultiAccountForm/ExtraSeatsAlert components consume emailAccountsAccess to 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 additions

PostgreSQL ≤ 11 cannot add more than one enum value in a single transaction. Because the file executes two ALTER TYPE … ADD VALUE statements 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() from next/server is 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 standard setImmediate / queueMicrotask or 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 trial

You create checkout.sessions with subscription_data: { trial_period_days: 7 }.
If the attached price also has a trial configured, Stripe rejects the request. Either remove the explicit trial_period_days or 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 pruned

Now 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.

Comment on lines +16 to +17
import { getStripe } from "@/ee/billing/stripe";
import { env } from "@/env";
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 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.ts

Length 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.

Suggested change
import { getStripe } from "@/ee/billing/stripe";
import { env } from "@/env";

hasAutomationRules: boolean;
hasColdEmailAccess: boolean;
hasAiAutomationAccess: boolean;
hasAiAccess: boolean;
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 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 ts

Length 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.

Comment on lines +213 to +215
<Button type="submit" loading={isExecuting}>
Save
</Button>
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ 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.

Suggested change
<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>
)}

Comment on lines +57 to +85

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]: {},
};
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

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.

Suggested change
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]: {},
};

Comment on lines +42 to 47
if (
["subscription_payment_success", "order_created"].includes(
payload.meta.event_name,
)
) {
return NextResponse.json({ ok: true });
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

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

Comment on lines 3 to 11
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;
};
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

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.

Suggested change
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;
};

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

♻️ Duplicate comments (4)
apps/web/utils/premium/index.ts (1)

3-14: ⚠️ Potential issue

isPremium ignores 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 the trackStripeEvent function

This 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: Add aria-disabled for accessibility parity

You already disable the CTA while it’s loading—great! Adding aria-disabled makes the state obvious to assistive technologies:

-        disabled={loading}
+        disabled={loading}
+        aria-disabled={loading}

30-33: ⚠️ Potential issue

Annual 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 in hasTierAccess.

The function returns a boolean indicating whether the user has access to a particular tier, but the variable storing the result is named hasAiAccess which 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 functions

The trackTrialStarted and trackSubscriptionTrialStarted functions 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 types

Most tracking functions use any for the attributes parameter, which reduces type safety. This could lead to mistakes when using these functions elsewhere in the codebase.

For example in the trackError function, 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 dynamically

The 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

📥 Commits

Reviewing files that changed from the base of the PR and between eff94eb and e2339cd.

📒 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: Improved hasUnsubscribeAccess implementation 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 isPremium function logic. The included statuses ("active" and "trialing") match the accepted premium statuses.

Comment on lines +245 to +261
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,
});
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ 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.

Suggested change
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,
});
}

Comment on lines +124 to +130
export async function trackStripeCheckoutCreated(email: string) {
return posthogCaptureEvent(email, "Stripe checkout created");
}

export async function trackStripeCheckoutCompleted(email: string) {
return posthogCaptureEvent(email, "Stripe checkout completed");
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ 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.

Suggested change
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,
});
}

@elie222 elie222 merged commit e7edcfb into main May 8, 2025
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

Comments