add stripe checkout
This commit is contained in:
parent
8e5e24c822
commit
31f3c4fef7
15 changed files with 900 additions and 69 deletions
339
package-lock.json
generated
339
package-lock.json
generated
|
|
@ -25,18 +25,22 @@
|
|||
"axios": "^1.3.5",
|
||||
"dayjs": "^1.11.7",
|
||||
"file-saver": "^2.0.5",
|
||||
"next": "^13.2.4",
|
||||
"micro": "^10.0.1",
|
||||
"micro-cors": "^0.1.1",
|
||||
"next": "^13.3.0",
|
||||
"next-auth": "^4.21.0",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-player": "^2.12.0",
|
||||
"react-popper": "^2.3.0",
|
||||
"stripe": "^12.1.1",
|
||||
"superjson": "1.12.2",
|
||||
"zod": "^3.21.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.32.3",
|
||||
"@types/eslint": "^8.21.3",
|
||||
"@types/micro-cors": "^0.1.3",
|
||||
"@types/node": "^18.15.5",
|
||||
"@types/prettier": "^2.7.2",
|
||||
"@types/react": "^18.0.28",
|
||||
|
|
@ -2336,11 +2340,28 @@
|
|||
"integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/micro": {
|
||||
"version": "7.3.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/micro/-/micro-7.3.7.tgz",
|
||||
"integrity": "sha512-MFsX7eCj0Tg3TtphOQvANNvNtFpya+s/rYOCdV6o+DFjOQPFi2EVRbBALjbbgZTXUaJP1Q281MJiJOD40d0UxQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/micro-cors": {
|
||||
"version": "0.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/micro-cors/-/micro-cors-0.1.3.tgz",
|
||||
"integrity": "sha512-f4aMXqEw9YjfdKX87m1LecvZJ2Mhz5maIHXjIvm5K6OTPe9auaTQwaFk4OZYS9zY6zdzfxqs2cEmwJAF7C9Y8A==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/micro": "^7.3.7"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "18.15.11",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.11.tgz",
|
||||
"integrity": "sha512-E5Kwq2n4SbMzQOn6wnmBjuK9ouqlURrcZDVfbo9ftDDTFt3nk7ZKK4GMOzoYgnpQJKcxwQw+lGaBvvlMo0qN/Q==",
|
||||
"dev": true
|
||||
"integrity": "sha512-E5Kwq2n4SbMzQOn6wnmBjuK9ouqlURrcZDVfbo9ftDDTFt3nk7ZKK4GMOzoYgnpQJKcxwQw+lGaBvvlMo0qN/Q=="
|
||||
},
|
||||
"node_modules/@types/prettier": {
|
||||
"version": "2.7.2",
|
||||
|
|
@ -2944,11 +2965,18 @@
|
|||
"node": ">=10.16.0"
|
||||
}
|
||||
},
|
||||
"node_modules/bytes": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz",
|
||||
"integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/call-bind": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
|
||||
"integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.1",
|
||||
"get-intrinsic": "^1.0.2"
|
||||
|
|
@ -3098,6 +3126,14 @@
|
|||
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/content-type": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz",
|
||||
"integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/cookie": {
|
||||
"version": "0.5.0",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
|
||||
|
|
@ -3255,6 +3291,14 @@
|
|||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/depd": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
|
||||
"integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/didyoumean": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
|
||||
|
|
@ -4162,8 +4206,7 @@
|
|||
"node_modules/function-bind": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
|
||||
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
|
||||
"dev": true
|
||||
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
|
||||
},
|
||||
"node_modules/function.prototype.name": {
|
||||
"version": "1.1.5",
|
||||
|
|
@ -4196,7 +4239,6 @@
|
|||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.0.tgz",
|
||||
"integrity": "sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.1",
|
||||
"has": "^1.0.3",
|
||||
|
|
@ -4353,7 +4395,6 @@
|
|||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
|
||||
"integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.1"
|
||||
},
|
||||
|
|
@ -4407,7 +4448,6 @@
|
|||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
|
||||
"integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
|
|
@ -4430,6 +4470,32 @@
|
|||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/http-errors": {
|
||||
"version": "1.7.3",
|
||||
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.3.tgz",
|
||||
"integrity": "sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==",
|
||||
"dependencies": {
|
||||
"depd": "~1.1.2",
|
||||
"inherits": "2.0.4",
|
||||
"setprototypeof": "1.1.1",
|
||||
"statuses": ">= 1.5.0 < 2",
|
||||
"toidentifier": "1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"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/ignore": {
|
||||
"version": "5.2.4",
|
||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz",
|
||||
|
|
@ -4477,8 +4543,7 @@
|
|||
"node_modules/inherits": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||
"dev": true
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
|
||||
},
|
||||
"node_modules/internal-slot": {
|
||||
"version": "1.0.5",
|
||||
|
|
@ -5034,6 +5099,35 @@
|
|||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/micro": {
|
||||
"version": "10.0.1",
|
||||
"resolved": "https://registry.npmjs.org/micro/-/micro-10.0.1.tgz",
|
||||
"integrity": "sha512-9uwZSsUrqf6+4FLLpiPj5TRWQv5w5uJrJwsx1LR/TjqvQmKC1XnGQ9OHrFwR3cbZ46YqPqxO/XJCOpWnqMPw2Q==",
|
||||
"dependencies": {
|
||||
"arg": "4.1.0",
|
||||
"content-type": "1.0.4",
|
||||
"raw-body": "2.4.1"
|
||||
},
|
||||
"bin": {
|
||||
"micro": "dist/src/bin/micro.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/micro-cors": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/micro-cors/-/micro-cors-0.1.1.tgz",
|
||||
"integrity": "sha512-6WqIahA5sbQR1Gjexp1VuWGFDKbZZleJb/gy1khNGk18a6iN1FdTcr3Q8twaxkV5H94RjxIBjirYbWCehpMBFw==",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/micro/node_modules/arg": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/arg/-/arg-4.1.0.tgz",
|
||||
"integrity": "sha512-ZWc51jO3qegGkVh8Hwpv636EkbesNV5ZNQPCtRa+0qytRYPEs9IYT9qITY9buezqUH5uqyzlWLcufrzU2rffdg=="
|
||||
},
|
||||
"node_modules/micromatch": {
|
||||
"version": "4.0.5",
|
||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz",
|
||||
|
|
@ -5284,7 +5378,6 @@
|
|||
"version": "1.12.3",
|
||||
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz",
|
||||
"integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==",
|
||||
"dev": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
|
|
@ -5851,6 +5944,20 @@
|
|||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/qs": {
|
||||
"version": "6.11.1",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.1.tgz",
|
||||
"integrity": "sha512-0wsrzgTz/kAVIeuxSjnpGC56rzYtr6JT/2BwEvMaPhFIoYa1aGO8LbzuU1R0uUYQkLpWBTOj0l/CLAJB64J6nQ==",
|
||||
"dependencies": {
|
||||
"side-channel": "^1.0.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/queue-microtask": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
|
||||
|
|
@ -5883,6 +5990,20 @@
|
|||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/raw-body": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.1.tgz",
|
||||
"integrity": "sha512-9WmIKF6mkvA0SLmA2Knm9+qj89e+j1zqgyn8aXGd7+nAduPoqgI9lO57SAZNn/Byzo5P7JhXTyg9PzaJbH73bA==",
|
||||
"dependencies": {
|
||||
"bytes": "3.1.0",
|
||||
"http-errors": "1.7.3",
|
||||
"iconv-lite": "0.4.24",
|
||||
"unpipe": "1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/react": {
|
||||
"version": "18.2.0",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz",
|
||||
|
|
@ -6084,6 +6205,11 @@
|
|||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/safer-buffer": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
|
||||
},
|
||||
"node_modules/scheduler": {
|
||||
"version": "0.23.0",
|
||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz",
|
||||
|
|
@ -6107,6 +6233,11 @@
|
|||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/setprototypeof": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz",
|
||||
"integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw=="
|
||||
},
|
||||
"node_modules/shebang-command": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||
|
|
@ -6132,7 +6263,6 @@
|
|||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
|
||||
"integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"call-bind": "^1.0.0",
|
||||
"get-intrinsic": "^1.0.2",
|
||||
|
|
@ -6178,6 +6308,14 @@
|
|||
"source-map": "^0.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/statuses": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
|
||||
"integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/stop-iteration-iterator": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz",
|
||||
|
|
@ -6295,6 +6433,18 @@
|
|||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/stripe": {
|
||||
"version": "12.1.1",
|
||||
"resolved": "https://registry.npmjs.org/stripe/-/stripe-12.1.1.tgz",
|
||||
"integrity": "sha512-vn74vXtZeJx18oGzA0AhL818euhLF/juCgkKrJfAS1Y0bp5/EzQKPuc/75qQUvY43nNGIkgOVb3kUBuyoeqEkA==",
|
||||
"dependencies": {
|
||||
"@types/node": ">=8.1.0",
|
||||
"qs": "^6.11.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.*"
|
||||
}
|
||||
},
|
||||
"node_modules/strnum": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz",
|
||||
|
|
@ -6553,6 +6703,14 @@
|
|||
"node": ">=8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/toidentifier": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz",
|
||||
"integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==",
|
||||
"engines": {
|
||||
"node": ">=0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/ts-interface-checker": {
|
||||
"version": "0.1.13",
|
||||
"resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
|
||||
|
|
@ -6680,6 +6838,14 @@
|
|||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"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/update-browserslist-db": {
|
||||
"version": "1.0.10",
|
||||
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz",
|
||||
|
|
@ -8523,11 +8689,28 @@
|
|||
"integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/micro": {
|
||||
"version": "7.3.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/micro/-/micro-7.3.7.tgz",
|
||||
"integrity": "sha512-MFsX7eCj0Tg3TtphOQvANNvNtFpya+s/rYOCdV6o+DFjOQPFi2EVRbBALjbbgZTXUaJP1Q281MJiJOD40d0UxQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"@types/micro-cors": {
|
||||
"version": "0.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/micro-cors/-/micro-cors-0.1.3.tgz",
|
||||
"integrity": "sha512-f4aMXqEw9YjfdKX87m1LecvZJ2Mhz5maIHXjIvm5K6OTPe9auaTQwaFk4OZYS9zY6zdzfxqs2cEmwJAF7C9Y8A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/micro": "^7.3.7"
|
||||
}
|
||||
},
|
||||
"@types/node": {
|
||||
"version": "18.15.11",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.11.tgz",
|
||||
"integrity": "sha512-E5Kwq2n4SbMzQOn6wnmBjuK9ouqlURrcZDVfbo9ftDDTFt3nk7ZKK4GMOzoYgnpQJKcxwQw+lGaBvvlMo0qN/Q==",
|
||||
"dev": true
|
||||
"integrity": "sha512-E5Kwq2n4SbMzQOn6wnmBjuK9ouqlURrcZDVfbo9ftDDTFt3nk7ZKK4GMOzoYgnpQJKcxwQw+lGaBvvlMo0qN/Q=="
|
||||
},
|
||||
"@types/prettier": {
|
||||
"version": "2.7.2",
|
||||
|
|
@ -8941,11 +9124,15 @@
|
|||
"streamsearch": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"bytes": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz",
|
||||
"integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg=="
|
||||
},
|
||||
"call-bind": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
|
||||
"integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"function-bind": "^1.1.1",
|
||||
"get-intrinsic": "^1.0.2"
|
||||
|
|
@ -9045,6 +9232,11 @@
|
|||
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
|
||||
"dev": true
|
||||
},
|
||||
"content-type": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz",
|
||||
"integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA=="
|
||||
},
|
||||
"cookie": {
|
||||
"version": "0.5.0",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
|
||||
|
|
@ -9158,6 +9350,11 @@
|
|||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="
|
||||
},
|
||||
"depd": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
|
||||
"integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ=="
|
||||
},
|
||||
"didyoumean": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
|
||||
|
|
@ -9856,8 +10053,7 @@
|
|||
"function-bind": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
|
||||
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
|
||||
"dev": true
|
||||
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
|
||||
},
|
||||
"function.prototype.name": {
|
||||
"version": "1.1.5",
|
||||
|
|
@ -9881,7 +10077,6 @@
|
|||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.0.tgz",
|
||||
"integrity": "sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"function-bind": "^1.1.1",
|
||||
"has": "^1.0.3",
|
||||
|
|
@ -9996,7 +10191,6 @@
|
|||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
|
||||
"integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"function-bind": "^1.1.1"
|
||||
}
|
||||
|
|
@ -10031,8 +10225,7 @@
|
|||
"has-symbols": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
|
||||
"integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
|
||||
"dev": true
|
||||
"integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A=="
|
||||
},
|
||||
"has-tostringtag": {
|
||||
"version": "1.0.0",
|
||||
|
|
@ -10043,6 +10236,26 @@
|
|||
"has-symbols": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"http-errors": {
|
||||
"version": "1.7.3",
|
||||
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.3.tgz",
|
||||
"integrity": "sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==",
|
||||
"requires": {
|
||||
"depd": "~1.1.2",
|
||||
"inherits": "2.0.4",
|
||||
"setprototypeof": "1.1.1",
|
||||
"statuses": ">= 1.5.0 < 2",
|
||||
"toidentifier": "1.0.0"
|
||||
}
|
||||
},
|
||||
"iconv-lite": {
|
||||
"version": "0.4.24",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
|
||||
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
|
||||
"requires": {
|
||||
"safer-buffer": ">= 2.1.2 < 3"
|
||||
}
|
||||
},
|
||||
"ignore": {
|
||||
"version": "5.2.4",
|
||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz",
|
||||
|
|
@ -10078,8 +10291,7 @@
|
|||
"inherits": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||
"dev": true
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
|
||||
},
|
||||
"internal-slot": {
|
||||
"version": "1.0.5",
|
||||
|
|
@ -10472,6 +10684,28 @@
|
|||
"integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
|
||||
"dev": true
|
||||
},
|
||||
"micro": {
|
||||
"version": "10.0.1",
|
||||
"resolved": "https://registry.npmjs.org/micro/-/micro-10.0.1.tgz",
|
||||
"integrity": "sha512-9uwZSsUrqf6+4FLLpiPj5TRWQv5w5uJrJwsx1LR/TjqvQmKC1XnGQ9OHrFwR3cbZ46YqPqxO/XJCOpWnqMPw2Q==",
|
||||
"requires": {
|
||||
"arg": "4.1.0",
|
||||
"content-type": "1.0.4",
|
||||
"raw-body": "2.4.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"arg": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/arg/-/arg-4.1.0.tgz",
|
||||
"integrity": "sha512-ZWc51jO3qegGkVh8Hwpv636EkbesNV5ZNQPCtRa+0qytRYPEs9IYT9qITY9buezqUH5uqyzlWLcufrzU2rffdg=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"micro-cors": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/micro-cors/-/micro-cors-0.1.1.tgz",
|
||||
"integrity": "sha512-6WqIahA5sbQR1Gjexp1VuWGFDKbZZleJb/gy1khNGk18a6iN1FdTcr3Q8twaxkV5H94RjxIBjirYbWCehpMBFw=="
|
||||
},
|
||||
"micromatch": {
|
||||
"version": "4.0.5",
|
||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz",
|
||||
|
|
@ -10630,8 +10864,7 @@
|
|||
"object-inspect": {
|
||||
"version": "1.12.3",
|
||||
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz",
|
||||
"integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==",
|
||||
"dev": true
|
||||
"integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g=="
|
||||
},
|
||||
"object-is": {
|
||||
"version": "1.1.5",
|
||||
|
|
@ -10963,6 +11196,14 @@
|
|||
"integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==",
|
||||
"dev": true
|
||||
},
|
||||
"qs": {
|
||||
"version": "6.11.1",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.1.tgz",
|
||||
"integrity": "sha512-0wsrzgTz/kAVIeuxSjnpGC56rzYtr6JT/2BwEvMaPhFIoYa1aGO8LbzuU1R0uUYQkLpWBTOj0l/CLAJB64J6nQ==",
|
||||
"requires": {
|
||||
"side-channel": "^1.0.4"
|
||||
}
|
||||
},
|
||||
"queue-microtask": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
|
||||
|
|
@ -10975,6 +11216,17 @@
|
|||
"integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==",
|
||||
"dev": true
|
||||
},
|
||||
"raw-body": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.1.tgz",
|
||||
"integrity": "sha512-9WmIKF6mkvA0SLmA2Knm9+qj89e+j1zqgyn8aXGd7+nAduPoqgI9lO57SAZNn/Byzo5P7JhXTyg9PzaJbH73bA==",
|
||||
"requires": {
|
||||
"bytes": "3.1.0",
|
||||
"http-errors": "1.7.3",
|
||||
"iconv-lite": "0.4.24",
|
||||
"unpipe": "1.0.0"
|
||||
}
|
||||
},
|
||||
"react": {
|
||||
"version": "18.2.0",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz",
|
||||
|
|
@ -11115,6 +11367,11 @@
|
|||
"is-regex": "^1.1.4"
|
||||
}
|
||||
},
|
||||
"safer-buffer": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
|
||||
},
|
||||
"scheduler": {
|
||||
"version": "0.23.0",
|
||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz",
|
||||
|
|
@ -11132,6 +11389,11 @@
|
|||
"lru-cache": "^6.0.0"
|
||||
}
|
||||
},
|
||||
"setprototypeof": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz",
|
||||
"integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw=="
|
||||
},
|
||||
"shebang-command": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||
|
|
@ -11151,7 +11413,6 @@
|
|||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
|
||||
"integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"call-bind": "^1.0.0",
|
||||
"get-intrinsic": "^1.0.2",
|
||||
|
|
@ -11185,6 +11446,11 @@
|
|||
"source-map": "^0.6.0"
|
||||
}
|
||||
},
|
||||
"statuses": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
|
||||
"integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA=="
|
||||
},
|
||||
"stop-iteration-iterator": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz",
|
||||
|
|
@ -11269,6 +11535,15 @@
|
|||
"integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
|
||||
"dev": true
|
||||
},
|
||||
"stripe": {
|
||||
"version": "12.1.1",
|
||||
"resolved": "https://registry.npmjs.org/stripe/-/stripe-12.1.1.tgz",
|
||||
"integrity": "sha512-vn74vXtZeJx18oGzA0AhL818euhLF/juCgkKrJfAS1Y0bp5/EzQKPuc/75qQUvY43nNGIkgOVb3kUBuyoeqEkA==",
|
||||
"requires": {
|
||||
"@types/node": ">=8.1.0",
|
||||
"qs": "^6.11.0"
|
||||
}
|
||||
},
|
||||
"strnum": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz",
|
||||
|
|
@ -11445,6 +11720,11 @@
|
|||
"is-number": "^7.0.0"
|
||||
}
|
||||
},
|
||||
"toidentifier": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz",
|
||||
"integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw=="
|
||||
},
|
||||
"ts-interface-checker": {
|
||||
"version": "0.1.13",
|
||||
"resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
|
||||
|
|
@ -11541,6 +11821,11 @@
|
|||
"which-boxed-primitive": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"unpipe": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
|
||||
"integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="
|
||||
},
|
||||
"update-browserslist-db": {
|
||||
"version": "1.0.10",
|
||||
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz",
|
||||
|
|
|
|||
|
|
@ -31,18 +31,22 @@
|
|||
"axios": "^1.3.5",
|
||||
"dayjs": "^1.11.7",
|
||||
"file-saver": "^2.0.5",
|
||||
"next": "^13.2.4",
|
||||
"micro": "^10.0.1",
|
||||
"micro-cors": "^0.1.1",
|
||||
"next": "^13.3.0",
|
||||
"next-auth": "^4.21.0",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-player": "^2.12.0",
|
||||
"react-popper": "^2.3.0",
|
||||
"stripe": "^12.1.1",
|
||||
"superjson": "1.12.2",
|
||||
"zod": "^3.21.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.32.3",
|
||||
"@types/eslint": "^8.21.3",
|
||||
"@types/micro-cors": "^0.1.3",
|
||||
"@types/node": "^18.15.5",
|
||||
"@types/prettier": "^2.7.2",
|
||||
"@types/react": "^18.0.28",
|
||||
|
|
|
|||
|
|
@ -58,14 +58,41 @@ model Session {
|
|||
}
|
||||
|
||||
model User {
|
||||
id String @id @default(cuid())
|
||||
name String?
|
||||
email String? @unique
|
||||
emailVerified DateTime?
|
||||
image String?
|
||||
accounts Account[]
|
||||
sessions Session[]
|
||||
videos Video[]
|
||||
id String @id @default(cuid())
|
||||
name String?
|
||||
email String? @unique
|
||||
emailVerified DateTime?
|
||||
image String?
|
||||
accounts Account[]
|
||||
sessions Session[]
|
||||
videos Video[]
|
||||
stripeCustomerId String?
|
||||
stripeSubscriptionId String?
|
||||
stripeSubscriptionStatus StripeSubscriptionStatus?
|
||||
}
|
||||
|
||||
enum StripeSubscriptionStatus {
|
||||
incomplete
|
||||
incomplete_expired
|
||||
trialing
|
||||
active
|
||||
past_due
|
||||
canceled
|
||||
unpaid
|
||||
paused
|
||||
}
|
||||
|
||||
model StripeEvent {
|
||||
id String @id @unique
|
||||
api_version String?
|
||||
data Json
|
||||
request Json?
|
||||
type String
|
||||
object String
|
||||
account String?
|
||||
created DateTime
|
||||
livemode Boolean
|
||||
pending_webhooks Int
|
||||
}
|
||||
|
||||
model VerificationToken {
|
||||
|
|
|
|||
94
src/components/Checkout.tsx
Normal file
94
src/components/Checkout.tsx
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
import { useRouter } from "next/router";
|
||||
import { api } from "~/utils/api";
|
||||
|
||||
export default function Checkout() {
|
||||
const { mutateAsync: createCheckoutSession } =
|
||||
api.stripe.createCheckoutSession.useMutation();
|
||||
const { push } = useRouter();
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<div className="mx-auto max-w-7xl px-6 pb-14 lg:px-8">
|
||||
<div className="mx-auto max-w-2xl sm:text-center">
|
||||
<h2 className="text-3xl font-bold tracking-tight text-gray-900 sm:text-4xl">
|
||||
Simple no-tricks pricing
|
||||
</h2>
|
||||
</div>
|
||||
<div className="mx-auto mt-16 max-w-2xl rounded-3xl ring-1 ring-gray-200 sm:mt-20 lg:mx-0 lg:flex lg:max-w-none">
|
||||
<div className="p-8 sm:p-10 lg:flex-auto">
|
||||
<h3 className="text-2xl font-bold tracking-tight text-gray-900">
|
||||
Pro plan
|
||||
</h3>
|
||||
<p className="mt-6 text-base leading-7 text-gray-600">
|
||||
Record and share unlimited videos. With this plan, you'll
|
||||
have access to all powerful features that help you create and
|
||||
share high-quality videos with ease.
|
||||
</p>
|
||||
<div className="mt-10 flex items-center gap-x-4">
|
||||
<h4 className="flex-none text-sm font-semibold leading-6 text-indigo-600">
|
||||
What’s included
|
||||
</h4>
|
||||
<div className="h-px flex-auto bg-gray-100"></div>
|
||||
</div>
|
||||
<ul
|
||||
role="list"
|
||||
className="mt-8 grid grid-cols-1 gap-4 text-sm leading-6 text-gray-600 sm:grid-cols-2 sm:gap-6"
|
||||
>
|
||||
{[
|
||||
"Versatile screen recording: Tab, desktop, any app, camera.",
|
||||
"Secure video sharing: Automatic link expiry.",
|
||||
"Powerful annotation tools: Text, drawing, arrows.",
|
||||
"Effortless editing: Trimming, removal of unwanted sections.",
|
||||
].map((x) => (
|
||||
<li key={x} className="flex gap-x-3">
|
||||
<svg
|
||||
className="h-6 w-5 flex-none text-indigo-600"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
{x}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<div className="-mt-2 p-2 lg:mt-0 lg:w-full lg:max-w-md lg:flex-shrink-0">
|
||||
<div className="h-full rounded-2xl bg-gray-50 py-10 text-center ring-1 ring-inset ring-gray-900/5 lg:flex lg:flex-col lg:justify-center lg:py-16">
|
||||
<div className="mx-auto max-w-xs px-8">
|
||||
<p className="mt-6 flex items-baseline justify-center gap-x-2">
|
||||
<span className="text-5xl font-bold tracking-tight text-gray-900">
|
||||
$5
|
||||
</span>
|
||||
<span className="text-sm font-semibold leading-6 tracking-wide text-gray-600">
|
||||
USD / mo
|
||||
</span>
|
||||
</p>
|
||||
<button
|
||||
onClick={() => {
|
||||
void createCheckoutSession().then(({ checkoutUrl }) => {
|
||||
if (checkoutUrl) {
|
||||
void push(checkoutUrl);
|
||||
}
|
||||
});
|
||||
}}
|
||||
className="mt-10 block w-full rounded-md bg-indigo-600 px-3 py-2 text-center text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
|
||||
>
|
||||
Get access
|
||||
</button>
|
||||
<p className="mt-6 text-xs leading-5 text-gray-600">
|
||||
Invoices and receipts available for easy company reimbursement
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
72
src/components/ProfileMenu.tsx
Normal file
72
src/components/ProfileMenu.tsx
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
import { Menu, Transition } from "@headlessui/react";
|
||||
import { Fragment } from "react";
|
||||
import { signOut } from "next-auth/react";
|
||||
import { useRouter } from "next/router";
|
||||
import { api } from "~/utils/api";
|
||||
|
||||
export default function ProfileMenu() {
|
||||
const { mutateAsync: createBillingPortalSession } =
|
||||
api.stripe.createBillingPortalSession.useMutation();
|
||||
const { push } = useRouter();
|
||||
|
||||
return (
|
||||
<Menu as="div" className="relative inline-block text-left">
|
||||
<Menu.Button className="flex rounded-full bg-gray-800 text-sm focus:outline-none focus:ring-2 focus:ring-white focus:ring-offset-2 focus:ring-offset-gray-800">
|
||||
<span className="sr-only">Open user menu</span>
|
||||
<img
|
||||
className="h-8 w-8 rounded-full"
|
||||
src="https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80"
|
||||
alt=""
|
||||
/>
|
||||
</Menu.Button>
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-100"
|
||||
enterFrom="transform opacity-0 scale-95"
|
||||
enterTo="transform opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75"
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Menu.Items className="absolute right-0 mt-2 origin-top-right divide-y divide-gray-100 rounded-md bg-white shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
|
||||
<div className="px-1 py-1 ">
|
||||
<Menu.Item>
|
||||
{({ active }) => (
|
||||
<div
|
||||
onClick={() => {
|
||||
void createBillingPortalSession().then(
|
||||
({ billingPortalUrl }) => {
|
||||
if (billingPortalUrl) {
|
||||
void push(billingPortalUrl);
|
||||
}
|
||||
}
|
||||
);
|
||||
}}
|
||||
className={`mx-2 flex h-8 w-40 cursor-pointer flex-row content-center rounded-md p-2 ${
|
||||
active ? "bg-gray-100" : ""
|
||||
}`}
|
||||
>
|
||||
<p className="leading-2 text-sm leading-4">
|
||||
Billing settings
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</Menu.Item>
|
||||
<Menu.Item>
|
||||
{({ active }) => (
|
||||
<div
|
||||
onClick={() => void signOut()}
|
||||
className={`mx-2 flex h-8 w-40 cursor-pointer flex-row content-center rounded-md p-2 ${
|
||||
active ? "bg-gray-100" : ""
|
||||
}`}
|
||||
>
|
||||
<p className="leading-2 text-sm leading-4">Sign out</p>
|
||||
</div>
|
||||
)}
|
||||
</Menu.Item>
|
||||
</div>
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
</Menu>
|
||||
);
|
||||
}
|
||||
|
|
@ -114,7 +114,7 @@ export default function VideoMoreMenu({ video }: Props) {
|
|||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Menu.Items className="absolute right-0 z-20 mt-2 origin-top-right divide-y divide-gray-100 rounded-md bg-white shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
|
||||
<Menu.Items className="absolute right-0 z-20 mt-2 origin-top-right divide-y divide-gray-100 rounded-md bg-white shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
|
||||
<div className="px-1 py-1 ">
|
||||
{items.map((item) => (
|
||||
<div className="h-8" key={item.name} {...item.props}>
|
||||
|
|
|
|||
12
src/env.mjs
12
src/env.mjs
|
|
@ -27,7 +27,10 @@ const server = z.object({
|
|||
AWS_REGION: z.string(),
|
||||
AWS_KEY_ID: z.string(),
|
||||
AWS_ACCESS_KEY: z.string(),
|
||||
AWS_BUCKET_NAME: z.string()
|
||||
AWS_BUCKET_NAME: z.string(),
|
||||
STRIPE_SECRET_KEY: z.string(),
|
||||
STRIPE_WEBHOOK_SECRET: z.string(),
|
||||
STRIPE_PRICE_ID: z.string()
|
||||
});
|
||||
|
||||
/**
|
||||
|
|
@ -36,6 +39,7 @@ const server = z.object({
|
|||
*/
|
||||
const client = z.object({
|
||||
// NEXT_PUBLIC_CLIENTVAR: z.string().min(1),
|
||||
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY: z.string()
|
||||
});
|
||||
|
||||
/**
|
||||
|
|
@ -58,7 +62,11 @@ const processEnv = {
|
|||
AWS_REGION: process.env.AWS_REGION,
|
||||
AWS_KEY_ID: process.env.AWS_KEY_ID,
|
||||
AWS_ACCESS_KEY: process.env.AWS_ACCESS_KEY,
|
||||
AWS_BUCKET_NAME: process.env.AWS_BUCKET_NAME
|
||||
AWS_BUCKET_NAME: process.env.AWS_BUCKET_NAME,
|
||||
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY: process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY,
|
||||
STRIPE_SECRET_KEY: process.env.STRIPE_SECRET_KEY,
|
||||
STRIPE_WEBHOOK_SECRET: process.env.STRIPE_WEBHOOK_SECRET,
|
||||
STRIPE_PRICE_ID: process.env.STRIPE_PRICE_ID
|
||||
};
|
||||
|
||||
// Don't touch the part below
|
||||
|
|
|
|||
110
src/pages/api/webhooks/stripe.ts
Normal file
110
src/pages/api/webhooks/stripe.ts
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { env } from "~/env.mjs";
|
||||
import { prisma } from "~/server/db";
|
||||
import type Stripe from "stripe";
|
||||
import { buffer } from "micro";
|
||||
import {
|
||||
handleInvoicePaid,
|
||||
handleSubscriptionCanceled,
|
||||
handleSubscriptionCreatedOrUpdated,
|
||||
} from "~/server/stripe-webhook-handlers";
|
||||
import { stripe } from "~/server/stripe";
|
||||
|
||||
// Stripe requires the raw body to construct the event.
|
||||
export const config = {
|
||||
api: {
|
||||
bodyParser: false,
|
||||
},
|
||||
};
|
||||
|
||||
const webhookSecret = env.STRIPE_WEBHOOK_SECRET;
|
||||
|
||||
export default async function handler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse
|
||||
) {
|
||||
if (req.method === "POST") {
|
||||
const buf = await buffer(req);
|
||||
const sig = req.headers["stripe-signature"];
|
||||
|
||||
let event: Stripe.Event;
|
||||
|
||||
try {
|
||||
event = stripe.webhooks.constructEvent(buf, sig as string, webhookSecret);
|
||||
|
||||
// Handle the event
|
||||
switch (event.type) {
|
||||
case "invoice.paid":
|
||||
// Used to provision services after the trial has ended.
|
||||
// The status of the invoice will show up as paid. Store the status in your database to reference when a user accesses your service to avoid hitting rate limits.
|
||||
await handleInvoicePaid({
|
||||
event,
|
||||
stripe,
|
||||
prisma,
|
||||
});
|
||||
break;
|
||||
case "customer.subscription.created":
|
||||
// Used to provision services as they are added to a subscription.
|
||||
await handleSubscriptionCreatedOrUpdated({
|
||||
event,
|
||||
prisma,
|
||||
});
|
||||
break;
|
||||
case "customer.subscription.updated":
|
||||
// Used to provision services as they are updated.
|
||||
await handleSubscriptionCreatedOrUpdated({
|
||||
event,
|
||||
prisma,
|
||||
});
|
||||
break;
|
||||
case "invoice.payment_failed":
|
||||
// If the payment fails or the customer does not have a valid payment method,
|
||||
// an invoice.payment_failed event is sent, the subscription becomes past_due.
|
||||
// Use this webhook to notify your user that their payment has
|
||||
// failed and to retrieve new card details.
|
||||
// Can also have Stripe send an email to the customer notifying them of the failure. See settings: https://dashboard.stripe.com/settings/billing/automatic
|
||||
break;
|
||||
case "customer.subscription.deleted":
|
||||
// handle subscription cancelled automatically based
|
||||
// upon your subscription settings.
|
||||
await handleSubscriptionCanceled({
|
||||
event,
|
||||
prisma,
|
||||
});
|
||||
break;
|
||||
default:
|
||||
// Unexpected event type
|
||||
}
|
||||
|
||||
// record the event in the database
|
||||
await prisma.stripeEvent.create({
|
||||
data: {
|
||||
id: event.id,
|
||||
type: event.type,
|
||||
object: event.object,
|
||||
api_version: event.api_version,
|
||||
account: event.account,
|
||||
created: new Date(event.created * 1000), // convert to milliseconds
|
||||
data: {
|
||||
object: event.data.object,
|
||||
previous_attributes: event.data.previous_attributes,
|
||||
},
|
||||
livemode: event.livemode,
|
||||
pending_webhooks: event.pending_webhooks,
|
||||
request: {
|
||||
id: event.request?.id,
|
||||
idempotency_key: event.request?.idempotency_key,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
res.json({ received: true });
|
||||
} catch (err) {
|
||||
res.status(400).send(err);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
res.setHeader("Allow", "POST");
|
||||
res.status(405).end("Method Not Allowed");
|
||||
}
|
||||
}
|
||||
|
|
@ -8,10 +8,12 @@ import { useRouter } from "next/router";
|
|||
import Image from "next/image";
|
||||
import VideoUploadModal from "~/components/VideoUploadModal";
|
||||
import { getTime } from "~/utils/getTime";
|
||||
import Checkout from "~/components/Checkout";
|
||||
import ProfileMenu from "~/components/ProfileMenu";
|
||||
|
||||
const VideoList: NextPage = () => {
|
||||
const router = useRouter();
|
||||
const { status } = useSession();
|
||||
const { status, data: session } = useSession();
|
||||
const { data: videos, isLoading } = api.video.getAll.useQuery();
|
||||
|
||||
if (status === "unauthenticated") {
|
||||
|
|
@ -28,37 +30,46 @@ const VideoList: NextPage = () => {
|
|||
<main className="flex h-screen min-h-screen flex-col items-center justify-center bg-gradient-to-b from-[#2e026d] to-[#15162c]">
|
||||
<div className="flex min-h-[62px] w-full items-center justify-between border-b border-solid border-b-[#E7E9EB] bg-white px-6">
|
||||
<span>Screenity</span>
|
||||
<div>
|
||||
<div className="flex flex-row items-center justify-center">
|
||||
<VideoUploadModal />
|
||||
{status === "authenticated" && (
|
||||
<div className="ml-3 flex items-center justify-center">
|
||||
<ProfileMenu />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex w-full grow items-start justify-center overflow-auto bg-[#fbfbfb] pt-14">
|
||||
<div className="flex-start grid w-full max-w-[1300px] grid-cols-[repeat(auto-fill,250px)] flex-row flex-wrap items-center justify-center gap-14 px-4 pb-16">
|
||||
{videos &&
|
||||
videos.map(({ title, id, createdAt }) => (
|
||||
<VideoCard
|
||||
title={title}
|
||||
id={id}
|
||||
createdAt={createdAt}
|
||||
key={id}
|
||||
/>
|
||||
))}
|
||||
{session?.user.stripeSubscriptionStatus === null ? (
|
||||
<Checkout />
|
||||
) : (
|
||||
<div className="flex-start grid w-full max-w-[1300px] grid-cols-[repeat(auto-fill,250px)] flex-row flex-wrap items-center justify-center gap-14 px-4 pb-16">
|
||||
{videos &&
|
||||
videos.map(({ title, id, createdAt }) => (
|
||||
<VideoCard
|
||||
title={title}
|
||||
id={id}
|
||||
createdAt={createdAt}
|
||||
key={id}
|
||||
/>
|
||||
))}
|
||||
|
||||
{isLoading ? (
|
||||
<>
|
||||
<VideoCardSkeleton />
|
||||
<VideoCardSkeleton />
|
||||
<VideoCardSkeleton />
|
||||
<VideoCardSkeleton />
|
||||
</>
|
||||
) : null}
|
||||
{isLoading ? (
|
||||
<>
|
||||
<VideoCardSkeleton />
|
||||
<VideoCardSkeleton />
|
||||
<VideoCardSkeleton />
|
||||
<VideoCardSkeleton />
|
||||
</>
|
||||
) : null}
|
||||
|
||||
{videos && videos?.length <= 0 ? (
|
||||
<div>
|
||||
<span>You do not have any recordings.</span>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
{videos && videos?.length <= 0 ? (
|
||||
<div>
|
||||
<span>You do not have any recordings.</span>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { createTRPCRouter } from "~/server/api/trpc";
|
||||
import { exampleRouter } from "~/server/api/routers/example";
|
||||
import { videoRouter } from "~/server/api/routers/video";
|
||||
import { stripeRouter } from "~/server/api/routers/stripe";
|
||||
|
||||
/**
|
||||
* This is the primary router for your server.
|
||||
|
|
@ -10,6 +11,7 @@ import { videoRouter } from "~/server/api/routers/video";
|
|||
export const appRouter = createTRPCRouter({
|
||||
example: exampleRouter,
|
||||
video: videoRouter,
|
||||
stripe: stripeRouter,
|
||||
});
|
||||
|
||||
// export type definition of API
|
||||
|
|
|
|||
80
src/server/api/routers/stripe.ts
Normal file
80
src/server/api/routers/stripe.ts
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
import { env } from "~/env.mjs";
|
||||
import { getOrCreateStripeCustomerIdForUser } from "~/server/stripe-webhook-handlers";
|
||||
import { createTRPCRouter, protectedProcedure } from "~/server/api/trpc";
|
||||
|
||||
export const stripeRouter = createTRPCRouter({
|
||||
createCheckoutSession: protectedProcedure.mutation(async ({ ctx }) => {
|
||||
const { stripe, session, prisma, req } = ctx;
|
||||
|
||||
const customerId = await getOrCreateStripeCustomerIdForUser({
|
||||
prisma,
|
||||
stripe,
|
||||
userId: session.user?.id,
|
||||
});
|
||||
|
||||
if (!customerId) {
|
||||
throw new Error("Could not create customer");
|
||||
}
|
||||
|
||||
const baseUrl =
|
||||
env.NODE_ENV === "development"
|
||||
? `http://${req.headers.host ?? "localhost:3000"}`
|
||||
: `https://${req.headers.host ?? env.NEXTAUTH_URL}`;
|
||||
|
||||
const checkoutSession = await stripe.checkout.sessions.create({
|
||||
customer: customerId,
|
||||
client_reference_id: session.user?.id,
|
||||
payment_method_types: ["card"],
|
||||
mode: "subscription",
|
||||
line_items: [
|
||||
{
|
||||
price: env.STRIPE_PRICE_ID,
|
||||
quantity: 1,
|
||||
},
|
||||
],
|
||||
success_url: `${baseUrl}/videos?checkoutSuccess=true`,
|
||||
cancel_url: `${baseUrl}/videos?checkoutCanceled=true`,
|
||||
subscription_data: {
|
||||
metadata: {
|
||||
userId: session.user?.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!checkoutSession) {
|
||||
throw new Error("Could not create checkout session");
|
||||
}
|
||||
|
||||
return { checkoutUrl: checkoutSession.url };
|
||||
}),
|
||||
createBillingPortalSession: protectedProcedure.mutation(async ({ ctx }) => {
|
||||
const { stripe, session, prisma, req } = ctx;
|
||||
|
||||
const customerId = await getOrCreateStripeCustomerIdForUser({
|
||||
prisma,
|
||||
stripe,
|
||||
userId: session.user?.id,
|
||||
});
|
||||
|
||||
if (!customerId) {
|
||||
throw new Error("Could not create customer");
|
||||
}
|
||||
|
||||
const baseUrl =
|
||||
env.NODE_ENV === "development"
|
||||
? `http://${req.headers.host ?? "localhost:3000"}`
|
||||
: `https://${req.headers.host ?? env.NEXTAUTH_URL}`;
|
||||
|
||||
const stripeBillingPortalSession =
|
||||
await stripe.billingPortal.sessions.create({
|
||||
customer: customerId,
|
||||
return_url: `${baseUrl}/videos`,
|
||||
});
|
||||
|
||||
if (!stripeBillingPortalSession) {
|
||||
throw new Error("Could not create billing portal session");
|
||||
}
|
||||
|
||||
return { billingPortalUrl: stripeBillingPortalSession.url };
|
||||
}),
|
||||
});
|
||||
|
|
@ -22,6 +22,8 @@ import { prisma } from "~/server/db";
|
|||
|
||||
type CreateContextOptions = {
|
||||
session: Session | null;
|
||||
req: NextApiRequest;
|
||||
res: NextApiResponse;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -39,6 +41,9 @@ const createInnerTRPCContext = (opts: CreateContextOptions) => {
|
|||
session: opts.session,
|
||||
prisma,
|
||||
s3,
|
||||
stripe,
|
||||
req: opts.req,
|
||||
res: opts.res,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
@ -56,6 +61,8 @@ export const createTRPCContext = async (opts: CreateNextContextOptions) => {
|
|||
|
||||
return createInnerTRPCContext({
|
||||
session,
|
||||
req,
|
||||
res,
|
||||
});
|
||||
};
|
||||
|
||||
|
|
@ -70,6 +77,8 @@ import { initTRPC, TRPCError } from "@trpc/server";
|
|||
import superjson from "superjson";
|
||||
import { ZodError } from "zod";
|
||||
import { s3 } from "~/server/aws/s3";
|
||||
import { stripe } from "~/server/stripe";
|
||||
import { type NextApiRequest, type NextApiResponse } from "next";
|
||||
|
||||
const t = initTRPC.context<typeof createTRPCContext>().create({
|
||||
transformer: superjson,
|
||||
|
|
|
|||
|
|
@ -20,15 +20,15 @@ declare module "next-auth" {
|
|||
interface Session extends DefaultSession {
|
||||
user: {
|
||||
id: string;
|
||||
stripeSubscriptionStatus: string;
|
||||
// ...other properties
|
||||
// role: UserRole;
|
||||
} & DefaultSession["user"];
|
||||
}
|
||||
|
||||
// interface User {
|
||||
// // ...other properties
|
||||
// // role: UserRole;
|
||||
// }
|
||||
interface User {
|
||||
stripeSubscriptionStatus: string;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -42,6 +42,7 @@ export const authOptions: NextAuthOptions = {
|
|||
...session,
|
||||
user: {
|
||||
...session.user,
|
||||
stripeSubscriptionStatus: user.stripeSubscriptionStatus,
|
||||
id: user.id,
|
||||
},
|
||||
}),
|
||||
|
|
|
|||
123
src/server/stripe-webhook-handlers.ts
Normal file
123
src/server/stripe-webhook-handlers.ts
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
import type { PrismaClient } from "@prisma/client";
|
||||
import type Stripe from "stripe";
|
||||
|
||||
// retrieves a Stripe customer id for a given user if it exists or creates a new one
|
||||
export const getOrCreateStripeCustomerIdForUser = async ({
|
||||
stripe,
|
||||
prisma,
|
||||
userId,
|
||||
}: {
|
||||
stripe: Stripe;
|
||||
prisma: PrismaClient;
|
||||
userId: string;
|
||||
}): Promise<string | null> => {
|
||||
const user = await prisma.user.findUnique({
|
||||
where: {
|
||||
id: userId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) throw new Error("User not found");
|
||||
|
||||
if (user.stripeCustomerId) {
|
||||
return user.stripeCustomerId;
|
||||
}
|
||||
|
||||
// create a new customer
|
||||
const customer = await stripe.customers.create({
|
||||
email: user.email ?? undefined,
|
||||
name: user.name ?? undefined,
|
||||
// use metadata to link this Stripe customer to internal user id
|
||||
metadata: {
|
||||
userId,
|
||||
},
|
||||
});
|
||||
|
||||
// update with new customer id
|
||||
const updatedUser = await prisma.user.update({
|
||||
where: {
|
||||
id: userId,
|
||||
},
|
||||
data: {
|
||||
stripeCustomerId: customer.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (updatedUser.stripeCustomerId) {
|
||||
return updatedUser.stripeCustomerId;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export const handleInvoicePaid = async ({
|
||||
event,
|
||||
stripe,
|
||||
prisma,
|
||||
}: {
|
||||
event: Stripe.Event;
|
||||
stripe: Stripe;
|
||||
prisma: PrismaClient;
|
||||
}) => {
|
||||
const invoice = event.data.object as Stripe.Invoice;
|
||||
const subscriptionId = invoice.subscription;
|
||||
const subscription = await stripe.subscriptions.retrieve(
|
||||
subscriptionId as string
|
||||
);
|
||||
const userId = subscription.metadata.userId;
|
||||
|
||||
// update user with subscription data
|
||||
await prisma.user.update({
|
||||
where: {
|
||||
id: userId,
|
||||
},
|
||||
data: {
|
||||
stripeSubscriptionId: subscription.id,
|
||||
stripeSubscriptionStatus: subscription.status,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const handleSubscriptionCreatedOrUpdated = async ({
|
||||
event,
|
||||
prisma,
|
||||
}: {
|
||||
event: Stripe.Event;
|
||||
prisma: PrismaClient;
|
||||
}) => {
|
||||
const subscription = event.data.object as Stripe.Subscription;
|
||||
const userId = subscription.metadata.userId;
|
||||
|
||||
// update user with subscription data
|
||||
await prisma.user.update({
|
||||
where: {
|
||||
id: userId,
|
||||
},
|
||||
data: {
|
||||
stripeSubscriptionId: subscription.id,
|
||||
stripeSubscriptionStatus: subscription.status,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const handleSubscriptionCanceled = async ({
|
||||
event,
|
||||
prisma,
|
||||
}: {
|
||||
event: Stripe.Event;
|
||||
prisma: PrismaClient;
|
||||
}) => {
|
||||
const subscription = event.data.object as Stripe.Subscription;
|
||||
const userId = subscription.metadata.userId;
|
||||
|
||||
// remove subscription data from user
|
||||
await prisma.user.update({
|
||||
where: {
|
||||
id: userId,
|
||||
},
|
||||
data: {
|
||||
stripeSubscriptionId: null,
|
||||
stripeSubscriptionStatus: null,
|
||||
},
|
||||
});
|
||||
};
|
||||
5
src/server/stripe.ts
Normal file
5
src/server/stripe.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import Stripe from "stripe";
|
||||
import { env } from "~/env.mjs";
|
||||
export const stripe = new Stripe(env.STRIPE_SECRET_KEY, {
|
||||
apiVersion: "2022-11-15",
|
||||
});
|
||||
Loading…
Reference in a new issue