-
Notifications
You must be signed in to change notification settings - Fork 75
Add stripe #408
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Add stripe #408
Changes from all commits
Commits
Show all changes
31 commits
Select commit
Hold shift + click to select a range
57bdbb3
feat: init web-shadcn with tailwind + shadcn/ui
xenia-levchenko e967682
feat: add base components
xenia-levchenko 36435f5
feat: add base components
xenia-levchenko 421bed3
feat: add base components
xenia-levchenko 438272f
feat: add base components
xenia-levchenko 78a7093
refactor
xenia-levchenko b4fddb6
feat: add layout
xenia-levchenko 6657670
add sign-up page
xenia-levchenko 659be70
add sign-in and forgot-pass pages
xenia-levchenko 858e1b5
add layout and rest pages
xenia-levchenko c242e2f
refactor
xenia-levchenko cd71095
add front for chat and sidebar
xenia-levchenko deffdbe
add LLM chat with streaming
xenia-levchenko 307b054
refactor
xenia-levchenko c75a6ec
refactor
xenia-levchenko 5afe2f3
refactor
xenia-levchenko 32a2937
add useApiMutation and useApiQuery
xenia-levchenko 4d1fffd
add navbar to layout
xenia-levchenko f69d486
add admin page
xenia-levchenko 79e0a49
refactor
xenia-levchenko 7453b5f
add useApiStreamMutation hook
xenia-levchenko a0c6222
fix mobile design
xenia-levchenko d03a6c3
refactor
xenia-levchenko 035c4ea
add schemas
xenia-levchenko 951621a
create stripe service
xenia-levchenko 854e22d
add endpoints
xenia-levchenko 41c1709
add webhook
xenia-levchenko 552b22d
add pricing page + fixes
xenia-levchenko 1f205d7
refactor
xenia-levchenko cf7e8b6
remove web-shadcn (not needed in this branch)
xenia-levchenko 65af0eb
pr fixes
xenia-levchenko File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
26 changes: 26 additions & 0 deletions
26
template/apps/api/src/resources/subscriptions/endpoints/createPortalSession.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| import { stripeService } from 'services'; | ||
| import createEndpoint from 'routes/createEndpoint'; | ||
|
|
||
| import config from 'config'; | ||
|
|
||
| export default createEndpoint({ | ||
| method: 'post', | ||
| path: '/create-portal-session', | ||
|
|
||
| async handler(ctx) { | ||
| const { user } = ctx.state; | ||
|
|
||
| const stripeId = user.stripeId || (await stripeService.createCustomer(user)); | ||
|
|
||
| if (!stripeId) { | ||
| return ctx.throwError('Failed to create Stripe customer'); | ||
| } | ||
|
|
||
| const session = await stripeService.billingPortal.sessions.create({ | ||
| customer: stripeId, | ||
| return_url: `${config.WEB_URL}/pricing?portal=true`, | ||
| }); | ||
|
|
||
| return { portalUrl: session.url }; | ||
| }, | ||
| }); |
65 changes: 65 additions & 0 deletions
65
template/apps/api/src/resources/subscriptions/endpoints/getCurrent.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,65 @@ | ||
| import Stripe from 'stripe'; | ||
|
|
||
| import { userService } from 'resources/users'; | ||
|
|
||
| import { stripeService } from 'services'; | ||
| import createEndpoint from 'routes/createEndpoint'; | ||
|
|
||
| export default createEndpoint({ | ||
| method: 'get', | ||
| path: '/current', | ||
|
|
||
| async handler(ctx) { | ||
| const { user } = ctx.state; | ||
|
|
||
| if (!user.subscription) { | ||
| return null; | ||
| } | ||
|
|
||
| const stripeSubscription = (await stripeService.subscriptions.retrieve( | ||
| user.subscription.subscriptionId, | ||
| )) as Stripe.Subscription; | ||
|
|
||
| if (stripeSubscription.status === 'canceled') { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Enum |
||
| await userService.atomic.updateOne({ _id: user._id }, { $unset: { subscription: '' } }); | ||
| return null; | ||
| } | ||
|
|
||
| const isCanceled = stripeSubscription.cancel_at_period_end || stripeSubscription.cancel_at !== null; | ||
|
|
||
| const stripePeriodEnd = | ||
| stripeSubscription.cancel_at ?? | ||
| (stripeSubscription as unknown as { current_period_end: number }).current_period_end; | ||
|
|
||
| const needsUpdate = | ||
| user.subscription.cancelAtPeriodEnd !== isCanceled || | ||
| user.subscription.status !== stripeSubscription.status || | ||
| user.subscription.priceId !== (stripeSubscription.items.data[0]?.price.id ?? user.subscription.priceId) || | ||
| user.subscription.currentPeriodEndDate !== stripePeriodEnd; | ||
|
|
||
| if (needsUpdate) { | ||
| const updatedSubscription = { | ||
| ...user.subscription, | ||
| priceId: stripeSubscription.items.data[0]?.price.id ?? user.subscription.priceId, | ||
| cancelAtPeriodEnd: isCanceled, | ||
| status: stripeSubscription.status, | ||
| currentPeriodStartDate: (stripeSubscription as unknown as { current_period_start: number }) | ||
| .current_period_start, | ||
| currentPeriodEndDate: stripePeriodEnd, | ||
| }; | ||
|
|
||
| await userService.atomic.updateOne({ _id: user._id }, { $set: { subscription: updatedSubscription } }); | ||
| user.subscription = updatedSubscription; | ||
| } | ||
|
|
||
| const product = await stripeService.products.retrieve(user.subscription.productId); | ||
|
|
||
| return { | ||
| ...user.subscription, | ||
| product: { | ||
| name: product.name, | ||
| images: product.images, | ||
| }, | ||
| }; | ||
| }, | ||
| }); | ||
51 changes: 51 additions & 0 deletions
51
template/apps/api/src/resources/subscriptions/endpoints/previewUpgrade.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| import { z } from 'zod'; | ||
|
|
||
| import { stripeService } from 'services'; | ||
| import createEndpoint from 'routes/createEndpoint'; | ||
|
|
||
| const schema = z.object({ | ||
| priceId: z.string().min(1, 'Price ID is required'), | ||
| }); | ||
|
|
||
| export default createEndpoint({ | ||
| method: 'get', | ||
| path: '/preview-upgrade', | ||
| schema, | ||
|
|
||
| async handler(ctx) { | ||
| const { user } = ctx.state; | ||
| const { priceId } = ctx.validatedData; | ||
|
|
||
| if (!user.stripeId) { | ||
| return ctx.throwError('Stripe account not found.'); | ||
| } | ||
|
|
||
| if (!user.subscription?.subscriptionId) { | ||
| return ctx.throwError('No active subscription found.'); | ||
| } | ||
|
|
||
| const subscriptionId = user.subscription.subscriptionId; | ||
| const subscriptionDetails = await stripeService.subscriptions.retrieve(subscriptionId); | ||
|
|
||
| const invoice = await stripeService.invoices.createPreview({ | ||
| customer: user.stripeId, | ||
| subscription: subscriptionId, | ||
| subscription_details: { | ||
| proration_behavior: 'always_invoice', | ||
| items: [ | ||
| { | ||
| id: subscriptionDetails.items.data[0].id, | ||
| price: priceId, | ||
| }, | ||
| ], | ||
| }, | ||
| }); | ||
|
|
||
| return { | ||
| subtotal: invoice.subtotal, | ||
| total: invoice.total, | ||
| amountDue: invoice.amount_due, | ||
| currency: invoice.currency, | ||
| }; | ||
| }, | ||
| }); |
50 changes: 50 additions & 0 deletions
50
template/apps/api/src/resources/subscriptions/endpoints/subscribe.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| import { z } from 'zod'; | ||
|
|
||
| import { stripeService } from 'services'; | ||
| import createEndpoint from 'routes/createEndpoint'; | ||
|
|
||
| import config from 'config'; | ||
|
|
||
| const schema = z.object({ | ||
| priceId: z.string().min(1, 'Price ID is required'), | ||
| }); | ||
|
|
||
| export default createEndpoint({ | ||
| method: 'post', | ||
| path: '/subscribe', | ||
| schema, | ||
|
|
||
| async handler(ctx) { | ||
| const { user } = ctx.state; | ||
| const { priceId } = ctx.validatedData; | ||
|
|
||
| const stripeId = user.stripeId || (await stripeService.createCustomer(user)); | ||
|
|
||
| if (!stripeId) { | ||
| return ctx.throwError('Failed to create Stripe customer'); | ||
| } | ||
|
|
||
| if (user.subscription) { | ||
| return ctx.throwError('You already have an active subscription. Please upgrade instead.'); | ||
| } | ||
|
|
||
| const session = await stripeService.checkout.sessions.create({ | ||
| mode: 'subscription', | ||
| customer: stripeId, | ||
| line_items: [ | ||
| { | ||
| quantity: 1, | ||
| price: priceId, | ||
| }, | ||
| ], | ||
| success_url: `${config.WEB_URL}/pricing?success=true`, | ||
| cancel_url: `${config.WEB_URL}/pricing?canceled=true`, | ||
| }); | ||
|
|
||
| if (!session.url) { | ||
| return ctx.throwError('Unable to create checkout session'); | ||
| } | ||
|
|
||
| return { checkoutUrl: session.url }; | ||
| }, | ||
| }); |
66 changes: 66 additions & 0 deletions
66
template/apps/api/src/resources/subscriptions/endpoints/upgrade.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,66 @@ | ||
| import Stripe from 'stripe'; | ||
| import { z } from 'zod'; | ||
|
|
||
| import { userService } from 'resources/users'; | ||
|
|
||
| import { stripeService } from 'services'; | ||
| import createEndpoint from 'routes/createEndpoint'; | ||
|
|
||
| const schema = z.object({ | ||
| priceId: z.string().min(1, 'Price ID is required'), | ||
| }); | ||
|
|
||
| export default createEndpoint({ | ||
| method: 'post', | ||
| path: '/upgrade', | ||
| schema, | ||
|
|
||
| async handler(ctx) { | ||
| const { user } = ctx.state; | ||
| const { priceId } = ctx.validatedData; | ||
|
|
||
| if (!user.stripeId) { | ||
| return ctx.throwError('Stripe account not found. Please contact support.'); | ||
| } | ||
|
|
||
| if (!user.subscription?.subscriptionId) { | ||
| return ctx.throwError('No active subscription found.'); | ||
| } | ||
|
|
||
| const subscriptionId = user.subscription.subscriptionId; | ||
|
|
||
| if (priceId === 'free') { | ||
| await stripeService.subscriptions.cancel(subscriptionId); | ||
| await userService.atomic.updateOne({ _id: user._id }, { $unset: { subscription: '' } }); | ||
| return { success: true, message: 'Subscription canceled' }; | ||
| } | ||
|
|
||
| const subscriptionDetails = (await stripeService.subscriptions.retrieve(subscriptionId)) as Stripe.Subscription; | ||
|
|
||
| const updatedStripeSubscription = (await stripeService.subscriptions.update(subscriptionId, { | ||
| proration_behavior: 'always_invoice', | ||
| cancel_at_period_end: false, | ||
| items: [ | ||
| { | ||
| id: subscriptionDetails.items.data[0].id, | ||
| price: priceId, | ||
| }, | ||
| ], | ||
| })) as Stripe.Subscription; | ||
|
|
||
| const updatedSubscription = { | ||
| ...user.subscription, | ||
| priceId: updatedStripeSubscription.items.data[0]?.price.id ?? priceId, | ||
| productId: updatedStripeSubscription.items.data[0]?.price.product as string, | ||
| status: updatedStripeSubscription.status, | ||
| cancelAtPeriodEnd: updatedStripeSubscription.cancel_at_period_end, | ||
| currentPeriodStartDate: (updatedStripeSubscription as unknown as { current_period_start: number }) | ||
| .current_period_start, | ||
| currentPeriodEndDate: (updatedStripeSubscription as unknown as { current_period_end: number }).current_period_end, | ||
| }; | ||
|
|
||
| await userService.atomic.updateOne({ _id: user._id }, { $set: { subscription: updatedSubscription } }); | ||
|
|
||
| return { success: true }; | ||
| }, | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider when create?