Skip to content

Conversation

@kizivat
Copy link
Contributor

@kizivat kizivat commented Apr 7, 2025

fixes #187

I had this implemented in my fork and noticed this issue in in your repo, so I wanted to chip in.

I'm unsure about storing the Stripe instance in event.locals in hook.server.ts but it seems to me as best approach, which could be accommodated in other parts of the app too, but if you can see some issues with it, I would love your input on it and I can recreate the instance directly in the client module.

Also, what could be up for discussion is whether or not the whole account deletion shouldn't go through if the subscription cancellation is not resolved successfully.

I decided to fail the deletion just so the user had a chance to try it again, ideally by first cancelling in the Stripe's Billing Portal.

Summary by CodeRabbit

  • New Features
    • Integrated a payment processing solution to enhance overall subscription management.
    • Improved the account deletion process to automatically handle cancellation of active and pending subscriptions, ensuring smoother user management and billing accuracy.
    • Added support for Stripe API integration to manage payment-related functionalities within the application.

@coderabbitai
Copy link

coderabbitai bot commented Apr 7, 2025

Walkthrough

The pull request adds a Stripe property to the application's type declarations and integrates Stripe initialization into the server hooks. The middleware now initializes a Stripe client with the provided API key, making it available in the request context. Additionally, the account deletion endpoint is updated to retrieve the user's Stripe customer details, list active subscriptions, and cancel them before account deletion.

Changes

File(s) Change Summary
src/app.d.ts, src/hooks.server.ts Added Stripe integration: updated the Locals interface with stripe: Stripe and implemented middleware logic to initialize and attach a Stripe client.
src/routes/(admin)/account/api/+page.server.ts Enhanced account deletion: updated the deleteAccount action to retrieve the Stripe customer ID, list active subscriptions, and cancel them.

Sequence Diagram(s)

sequenceDiagram
    participant C as Client
    participant M as Middleware
    participant D as DeleteAccount Handler
    participant DB as Database
    participant S as Stripe API

    C->>M: Send account deletion request
    M->>M: Initialize Supabase, Auth, and Stripe client
    M->>D: Forward request with attached Stripe client
    D->>DB: Query for Stripe customer ID
    DB-->>D: Return customer ID
    D->>S: Request list of active subscriptions
    S-->>D: Return subscriptions list
    D->>S: Cancel active subscriptions (in parallel)
    D-->>C: Return deletion status
Loading

Assessment against linked issues

Objective (Linked Issue) Addressed Explanation
Cancel user's active subscription upon account deletion (#187)

Poem

In fields of code I softly hop,
Stripe integration makes my heart stop.
Subscriptions canceled, worries at bay,
A clever rabbit sings this day.
Hop along, for changes pave the way!
🐇🌟

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

npm error Exit handler never called!
npm error This is an error with npm itself. Please report this error at:
npm error https://github.com/npm/cli/issues
npm error A complete log of this run can be found in: /.npm/_logs/2025-06-04T22_13_16_941Z-debug-0.log


📜 Recent review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 3e65775 and 1b0a4d9.

📒 Files selected for processing (1)
  • src/routes/(admin)/account/api/+page.server.ts (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/routes/(admin)/account/api/+page.server.ts
✨ Finishing Touches
  • 📝 Generate Docstrings

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

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • 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 explain this code block.
    • @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 explain its main purpose.
    • @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 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 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 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 for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@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: 2

🧹 Nitpick comments (3)
src/app.d.ts (1)

19-19: Type declaration looks good but possibly needs nullability.

The stripe property is currently declared as non-nullable, which means TypeScript will expect it to always be defined. Consider whether this property should be optional or nullable if there are cases where the Stripe instance might not be initialized.

-      stripe: Stripe
+      stripe: Stripe | null

Alternatively, if you're certain the Stripe instance will always be available when needed, the current non-nullable type is appropriate.

src/hooks.server.ts (1)

101-106: Stripe initialization looks good with a consideration.

The implementation initializes the Stripe client with a specific API version. This follows best practices to avoid unexpected breaking changes when Stripe updates their API.

However, consider extracting the API version to a configuration constant rather than hardcoding it, which would make future updates easier.

+const STRIPE_API_VERSION = "2023-08-16"
+
 const stripe: Handle = async ({ event, resolve }) => {
   event.locals.stripe = new Stripe(PRIVATE_STRIPE_API_KEY, {
-    apiVersion: "2023-08-16",
+    apiVersion: STRIPE_API_VERSION,
   })
   return resolve(event)
 }
src/routes/(admin)/account/api/+page.server.ts (1)

229-236: Optimize the Promise handling.

The current implementation creates a Promise variable and then immediately uses Promise.all with a single-element array, which adds unnecessary complexity.

-  const subscriptionsPromise = stripe.subscriptions.list({
-    customer: customer.stripe_customer_id,
-    limit: 100,
-  })
-
-  const [{ data: subscriptions }] = await Promise.all([
-    subscriptionsPromise,
-  ])
+  const { data: subscriptions } = await stripe.subscriptions.list({
+    customer: customer.stripe_customer_id,
+    limit: 100,
+  })
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 05b68e2 and 51d2491.

📒 Files selected for processing (3)
  • src/app.d.ts (2 hunks)
  • src/hooks.server.ts (3 hunks)
  • src/routes/(admin)/account/api/+page.server.ts (3 hunks)
🔇 Additional comments (8)
src/app.d.ts (1)

2-2: Good addition of Stripe type import.

This is necessary to properly type the Stripe instance in the Locals interface.

src/hooks.server.ts (3)

2-5: Clean import reorganization.

Properly structured imports with the Stripe API key.


14-14: Correct Stripe import.

The import is properly added for the Stripe package.


108-108: Sequence configuration is good.

The handle sequence is properly updated to include the new stripe middleware.

src/routes/(admin)/account/api/+page.server.ts (4)

183-183: Properly updated function parameter.

The stripe instance is now correctly included in the destructured locals parameter.


212-224: Good retrieval of Stripe customer ID.

The code properly fetches the user's Stripe customer ID from the database and handles potential errors.


242-246: Proper implementation of cancellation with Promise.all.

Using Promise.all for parallel cancellation of subscriptions is efficient and correctly implemented.


238-240:

✅ Verification successful

Fix subscription status typo.

The status "trailing" is incorrect, it should be "trialing" according to Stripe's API documentation.

 const currentSubscriptions = subscriptions.filter((sub) =>
-  ["active", "trailing", "past_due"].includes(sub.status),
+  ["active", "trialing", "past_due"].includes(sub.status),
 )

🌐 Web query:

What are the valid subscription statuses in Stripe API?

💡 Result:

Stripe subscriptions can have one of seven possible statuses: incomplete, incomplete_expired, trialing, active, past_due, canceled, or unpaid[1][4]. Here's a breakdown of each status:

  1. incomplete: Occurs when the first payment attempt fails during subscription creation. This status applies only to the initial payment[1][4].
  2. incomplete_expired: Automatically set if a subscription remains incomplete for 23 hours without successful payment[1].
  3. trialing: Active during a free trial period before the first full billing cycle[1][4].
  4. active: The subscription is in good standing, with payments successfully processed[1][4].
  5. past_due: Triggered when a recurring payment fails after the initial subscription creation[1][4].
  6. canceled: The subscription has been terminated and cannot be reactivated[1][4].
  7. unpaid: Similar to canceled, but leaves invoices open for potential future payment if the customer updates their payment method[1][4].

The status transitions depend on payment success/failure and retry settings configured in the Stripe Dashboard[4]. For example, failed payments typically move subscriptions to past_due, while prolonged failures may transition them to canceled or unpaid based on recovery rules[4][9].

Citations:


Critical: Replace invalid subscription status "trailing" with "trialing".
According to Stripe's API, the valid status is "trialing", not "trailing". Please update the code accordingly:

  • File: src/routes/(admin)/account/api/+page.server.ts
  • Lines: 238-240
 const currentSubscriptions = subscriptions.filter((sub) =>
-  ["active", "trailing", "past_due"].includes(sub.status),
+  ["active", "trialing", "past_due"].includes(sub.status),
 )

Copy link
Contributor

@scosman scosman left a comment

Choose a reason for hiding this comment

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

Thanks for the PR!

There's 1 issue we'd definitely need to fix (returning plaintext password).

I'm also torn on approach: I'm thinking we just show an error that says something like "You have an active subscription - cancel it from /billing before deleting your account"? It gives the user an extra chance to realize the impact of the change (paid services are about to stop working and there's no way of turning them back on if this is something mission critical). I could see some apps wanting this approach, and others wanting the warning, but the warning seems safer for a boilerplate/template.

As soon as it's canceled (even if still active until end of billing cycle) we should allow deleting the account.

console.error("Error getting stripe customer ID.", cusError)
return fail(500, {
errorMessage: "Unknown error. If this persists please contact us.",
currentPassword,
Copy link
Contributor

Choose a reason for hiding this comment

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

definitely shouldn't be returning their password in an error message in plaintext

Copy link
Contributor Author

Choose a reason for hiding this comment

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

wow 🥶 thanks for noticing that!

@kizivat
Copy link
Contributor Author

kizivat commented Jun 4, 2025

You're right it may be problematic that the user may not realize that he'll no longer be able to use the services.

To solve for this, we could add a prompt that asks the user to confirm they want to cancel their subscriptions.

On the other hand, if we want to take such cases into account, maybe the fully correct solution would be to just cancel the subscriptions and then "schedule" the full account deletion only after their subscriptions run out. Or ask them to come back and retry the deletion after their paid period.

I'm not sure whether the above would be what most people deleting their account would expect. 🤔

@scosman
Copy link
Contributor

scosman commented Jun 6, 2025

@kizivat Let's just keep it simple? How about: if you click delete with an active subscription, show a message "You have an active subscription. If you want to delete your account, cancel it first." with [Cancel] and [View Subscription] buttons? Saves a lot of the complexity and is clear for the user. Keep the server side check they don't have an active sub, but that's it.

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.

Deleting account does not cancel subscription

2 participants