add stripe checkout

This commit is contained in:
MarconLP 2023-04-16 13:05:18 +02:00
parent 8e5e24c822
commit 31f3c4fef7
No known key found for this signature in database
GPG key ID: A08A9C8B623F5EA5
15 changed files with 900 additions and 69 deletions

339
package-lock.json generated
View file

@ -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",

View file

@ -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",

View file

@ -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 {

View 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&apos;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">
Whats 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>
);
}

View 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>
);
}

View file

@ -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}>

View file

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

View 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");
}
}

View file

@ -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>
</>

View file

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

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

View file

@ -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,

View file

@ -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,
},
}),

View 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
View 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",
});