diff --git a/next.config.mjs b/next.config.mjs index dc82a4d2..4a049f2c 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -1,26 +1,24 @@ /** @type {import('next').NextConfig} */ const nextConfig = { - images: { - remotePatterns: [ - { - protocol: 'https', - hostname: 'oaidalleapiprodscus.blob.core.windows.net', - port: '', - }, - { - protocol: "https", - hostname: "vita-juliomeza2510-mybucket-uvkhksmm.s3.us-east-1.amazonaws.com", - port: '' - } - ], - }, - reactStrictMode: false, - experimental: { - serverComponentsExternalPackages: [ - 'puppeteer-core', - '@sparticuz/chromium' - ] - } -}; + images: { + remotePatterns: [ + { + protocol: 'https', + hostname: 'oaidalleapiprodscus.blob.core.windows.net', + port: '', + }, + { + protocol: 'https', + hostname: + 'vita-juliomeza2510-mybucket-uvkhksmm.s3.us-east-1.amazonaws.com', + port: '', + }, + ], + }, + reactStrictMode: false, + experimental: { + serverComponentsExternalPackages: ['puppeteer-core', '@sparticuz/chromium'], + }, +} -export default nextConfig; +export default nextConfig diff --git a/package-lock.json b/package-lock.json index 24e81d1f..06feb1a9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -49,6 +49,7 @@ "postgres": "^3.4.4", "puppeteer": "^22.10.0", "puppeteer-core": "^22.10.0", + "raw-body": "^2.5.2", "reacharts": "^0.4.5", "react": "^18.3.1", "react-circular-progressbar": "^2.1.0", @@ -61,6 +62,7 @@ "react-speech-recognition": "^3.10.0", "recharts": "^2.12.7", "sst": "^3.0.32", + "stripe": "^15.9.0", "sweetalert2": "^11.11.0", "tailwind-merge": "^2.3.0", "tailwindcss-animate": "^1.0.7", @@ -8777,6 +8779,14 @@ "node": ">=10.16.0" } }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/cac": { "version": "6.7.14", "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", @@ -10253,6 +10263,14 @@ "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==" }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/dependency-tree": { "version": "11.0.1", "resolved": "https://registry.npmjs.org/dependency-tree/-/dependency-tree-11.0.1.tgz", @@ -13092,6 +13110,21 @@ "entities": "^4.4.0" } }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/http-proxy-agent": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", @@ -16769,6 +16802,31 @@ "safe-buffer": "^5.1.0" } }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/raw-body/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/reacharts": { "version": "0.4.5", "resolved": "https://registry.npmjs.org/reacharts/-/reacharts-0.4.5.tgz", @@ -17575,8 +17633,7 @@ "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "node_modules/sass-lookup": { "version": "6.0.1", @@ -17692,6 +17749,11 @@ "node": ">= 0.4" } }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, "node_modules/shallow-equal": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/shallow-equal/-/shallow-equal-3.1.0.tgz", @@ -18083,7 +18145,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "dev": true, "engines": { "node": ">= 0.8" } @@ -18399,6 +18460,32 @@ "integrity": "sha512-WriZw1luRMlmV3LGJaR6QOJjWwgLUTf89OwT2lUOyjX2dJGBwgmIkbcz+7WFZjrZM635JOIR517++e/67CP9dQ==", "dev": true }, + "node_modules/stripe": { + "version": "15.9.0", + "resolved": "https://registry.npmjs.org/stripe/-/stripe-15.9.0.tgz", + "integrity": "sha512-C7NAK17wGr6DOybxThO0n1zVcqSShWno7kx/UcJorQ/nWZ5KnfHQ38DUTb9NgizC8TCII3BPIhqq6Zc/1aUkrg==", + "dependencies": { + "@types/node": ">=8.1.0", + "qs": "^6.11.0" + }, + "engines": { + "node": ">=12.*" + } + }, + "node_modules/stripe/node_modules/qs": { + "version": "6.12.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.12.1.tgz", + "integrity": "sha512-zWmv4RSuB9r2mYQw3zxQuHWeU+42aKi1wWig/j4ele4ygELZ7PEO6MM7rim9oAQH2A5MWfsAVf/jPvTPgCbvUQ==", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/strnum": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz", @@ -18967,6 +19054,14 @@ "node": ">=8.0" } }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "engines": { + "node": ">=0.6" + } + }, "node_modules/toposort": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz", @@ -19689,6 +19784,14 @@ "node": ">= 10.0.0" } }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/untildify": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", diff --git a/package.json b/package.json index 3f07413b..a2806c26 100644 --- a/package.json +++ b/package.json @@ -61,6 +61,7 @@ "postgres": "^3.4.4", "puppeteer": "^22.10.0", "puppeteer-core": "^22.10.0", + "raw-body": "^2.5.2", "reacharts": "^0.4.5", "react": "^18.3.1", "react-circular-progressbar": "^2.1.0", @@ -73,6 +74,7 @@ "react-speech-recognition": "^3.10.0", "recharts": "^2.12.7", "sst": "^3.0.32", + "stripe": "^15.9.0", "sweetalert2": "^11.11.0", "tailwind-merge": "^2.3.0", "tailwindcss-animate": "^1.0.7", diff --git a/src/app/(pages)/healthdata/page.tsx b/src/app/(pages)/healthdata/page.tsx index 2375eed2..a2d22bc7 100644 --- a/src/app/(pages)/healthdata/page.tsx +++ b/src/app/(pages)/healthdata/page.tsx @@ -46,7 +46,7 @@ const HealthData = () => { if (!data) { return } else { - router.replace('/plan') + router.replace('/home') } } catch (error) { console.log(error) diff --git a/src/app/(pages)/plan/page.tsx b/src/app/(pages)/plan/page.tsx index 62ae7475..169813fe 100644 --- a/src/app/(pages)/plan/page.tsx +++ b/src/app/(pages)/plan/page.tsx @@ -1,12 +1,14 @@ 'use client' +import axios from 'axios' import { NextPage } from 'next' -import Link from 'next/link' +import { useRouter } from 'next/navigation' interface Plan { name: string price: number features: string[] - link: string + priceId: string + allowTrial: boolean } const plans: Plan[] = [ @@ -21,7 +23,8 @@ const plans: Plan[] = [ 'Red Social para compartir progreso y apoyar a otros.', 'Perfil Médico', ], - link: 'https://buy.stripe.com/test_5kA4jc54DcqW2zKbII', + priceId: 'price_1PODCEA5dyQt5UTQT1A5yBVQ', + allowTrial: false, }, { name: 'Bienestar Plus', @@ -34,7 +37,8 @@ const plans: Plan[] = [ 'Recordatorios automáticos en Whatsapp.', 'Retos mensuales e insignias por logros.', ], - link: 'https://buy.stripe.com/test_00g8zs68HbmSb6gdQR', + priceId: 'price_1PODQFA5dyQt5UTQ9ejvVNjG', + allowTrial: true, }, { name: 'Bienestar Total', @@ -46,7 +50,8 @@ const plans: Plan[] = [ 'Chatbot por Whatsapp.', 'Acceso prioritario a las nuevas funciones en desarrollo.', ], - link: 'https://buy.stripe.com/test_dR68zsbt1fD8eisdQS', + priceId: 'price_1PODRUA5dyQt5UTQqsDYIfuQ', + allowTrial: false, }, ] @@ -68,6 +73,21 @@ const PricingPage: NextPage = () => { } const PlanCard: React.FC<{ plan: Plan }> = ({ plan }) => { + const router = useRouter() + + const processPayment = async () => { + try { + const res = await axios.post('/api/stripe/payment', { + priceId: plan.priceId, + allowTrial: plan.allowTrial, + }) + const data = res.data + router.push(data.url) + } catch (error) { + console.log(error) + } + } + return (
@@ -85,12 +105,12 @@ const PlanCard: React.FC<{ plan: Plan }> = ({ plan }) => { )}
- Seleccionar - +
) diff --git a/src/app/api/stripe/payment/route.ts b/src/app/api/stripe/payment/route.ts new file mode 100644 index 00000000..af1f4fd1 --- /dev/null +++ b/src/app/api/stripe/payment/route.ts @@ -0,0 +1,46 @@ +import { authOptions } from '@/src/lib/auth/authOptions' +import { stripe } from '@/src/lib/stripe/stripe' +import { getServerSession } from 'next-auth' +import { NextResponse } from 'next/server' +import Stripe from 'stripe' + +export async function POST(req: Request) { + const body = await req.json() + const session = await getServerSession(authOptions) + const { priceId, allowTrial } = body + + const stripeConfig: Stripe.Checkout.SessionCreateParams = { + success_url: 'http://localhost:3000/home', + cancel_url: 'http://localhost:3000', + payment_method_types: ['card'], + mode: 'subscription', + billing_address_collection: 'auto', + customer_email: session?.user.email, + allow_promotion_codes: true, + line_items: [ + { + price: priceId, + quantity: 1, + }, + ], + metadata: { + userId: session?.user.id, + }, + subscription_data: { + trial_settings: { + end_behavior: { + missing_payment_method: 'cancel', + }, + }, + }, + payment_method_collection: 'if_required', + } + + if (stripeConfig.subscription_data && allowTrial) { + stripeConfig.subscription_data.trial_period_days = 7 + } + + const res = await stripe.checkout.sessions.create(stripeConfig) + + return NextResponse.json({ url: res.url }, { status: 200 }) +} diff --git a/src/app/api/stripe/webhook/route.ts b/src/app/api/stripe/webhook/route.ts new file mode 100644 index 00000000..2682187c --- /dev/null +++ b/src/app/api/stripe/webhook/route.ts @@ -0,0 +1,43 @@ +import { db } from '@/src/db/drizzle' +import { user } from '@/src/db/schema/schema' +import configuration from '@/src/lib/environment/config' +import { stripe } from '@/src/lib/stripe/stripe' +import { eq } from 'drizzle-orm' +import { NextResponse } from 'next/server' +import Stripe from 'stripe' + +export async function POST(req: Request) { + const body = await req.text() + const signature = req.headers.get('stripe-signature') as string + const webhookSecret = configuration.stripeWebhookSecret + + if (!webhookSecret) + return new NextResponse('Missing Webhook Secret', { status: 500 }) + + if (!signature) + return new NextResponse('Missing Stripe Signature', { status: 400 }) + + let event: Stripe.Event | null = null + + try { + event = stripe.webhooks.constructEvent(body, signature, webhookSecret) + if (event.type === 'checkout.session.completed') { + const session = event.data.object + const subscriptionId = session.subscription + if (session.metadata) { + const userId = session.metadata.userId + await db + .update(user) + .set({ + membership: subscriptionId?.toString(), + membershipTime: new Date(), + }) + .where(eq(user.idUser, Number(userId))) + } + } + return NextResponse.json('OK', { status: 200 }) + } catch (error) { + console.log(error) + return NextResponse.json('Invalid Stripe Signature', { status: 400 }) + } +} diff --git a/src/lib/environment/config.ts b/src/lib/environment/config.ts index 23e9e01b..c8c2f10c 100644 --- a/src/lib/environment/config.ts +++ b/src/lib/environment/config.ts @@ -20,6 +20,8 @@ const config = nextAuthSecret: process.env.NEXTAUTH_SECRET!, nextPublicSecret: process.env.NEXT_PUBLIC_SECRET!, geminiApiKey: Resource.GeminiApiKey.value, + stripeSecretKey: Resource.StripeSecretKey.value, + stripeWebhookSecret: Resource.StripeWebhookSecret.value, } : { nodeEnv: process.env.NODE_ENV!, @@ -36,6 +38,8 @@ const config = webhookVerifyToken: process.env.WEBHOOK_VERIFY_TOKEN, graphApiToken: process.env.GRAPH_API_TOKEN, geminiApiKey: process.env.GEMINI_API_KEY, + stripeSecretKey: process.env.STRIPE_SECRET_KEY!, + stripeWebhookSecret: process.env.STRIPE_WEBHOOK_SECRET!, } export default config diff --git a/src/lib/stripe/stripe.ts b/src/lib/stripe/stripe.ts new file mode 100644 index 00000000..84359335 --- /dev/null +++ b/src/lib/stripe/stripe.ts @@ -0,0 +1,7 @@ +import Stripe from 'stripe' +import config from '../environment/config' + +export const stripe = new Stripe(config.stripeSecretKey, { + apiVersion: '2024-04-10', + typescript: true, +}) diff --git a/sst-env.d.ts b/sst-env.d.ts index ba05d38c..bf819fd4 100644 --- a/sst-env.d.ts +++ b/sst-env.d.ts @@ -45,6 +45,14 @@ declare module "sst" { type: "sst.sst.Secret" value: string } + StripeSecretKey: { + type: "sst.sst.Secret" + value: string + } + StripeWebhookSecret: { + type: "sst.sst.Secret" + value: string + } WebhookVerifyToken: { type: "sst.sst.Secret" value: string diff --git a/sst.config.ts b/sst.config.ts index fa477fc2..9b092ae0 100644 --- a/sst.config.ts +++ b/sst.config.ts @@ -18,6 +18,8 @@ export default $config({ const WebhookVerifyToken = new sst.Secret('WebhookVerifyToken') const GraphApiToken = new sst.Secret('GraphApiToken') const GeminiApiKey = new sst.Secret('GeminiApiKey') + const StripeSecretKey = new sst.Secret("StripeSecretKey") + const StripeWebhookSecret = new sst.Secret("StripeWebhookSecret") const database = new sst.aws.Postgres('MyDatabase', { scaling: { @@ -52,6 +54,8 @@ export default $config({ WebhookVerifyToken, GraphApiToken, GeminiApiKey, + StripeSecretKey, + StripeWebhookSecret ], environment: { NEXTAUTH_URL: process.env.NEXTAUTH_URL!,