@@ -23,6 +27,12 @@ async function signout() {
logged in as: {{ user.email }}: Sign Out
Not Logged in
+
+ Boilerplate
+ | Dashboard
+ | Pricing
+ | Account
+
diff --git a/lib/services/user.account.service.ts b/lib/services/user.account.service.ts
index fafb2ad..4505eab 100644
--- a/lib/services/user.account.service.ts
+++ b/lib/services/user.account.service.ts
@@ -40,6 +40,34 @@ export default class UserAccountService {
});
}
+ async getAccountById(account_id: number): Promise {
+ return this.prisma.account.findFirstOrThrow({
+ where: { id: account_id },
+ });
+ }
+
+ async updateAccountStipeCustomerId (account_id: number, stripe_customer_id: string){
+ return await this.prisma.account.update({
+ where: { id: account_id },
+ data: {
+ stripe_customer_id,
+ }
+ })
+ }
+
+ async updateStripeSubscriptionDetailsForAccount (stripe_customer_id: string, stripe_subscription_id: string, current_period_ends: Date){
+ const account = await this.prisma.account.findFirstOrThrow({
+ where: {stripe_customer_id}
+ });
+ return await this.prisma.account.update({
+ where: { id: account.id },
+ data: {
+ stripe_subscription_id,
+ current_period_ends
+ }
+ })
+ }
+
async createUser( supabase_uid: string, display_name: string ): Promise {
const trialPlan = await this.prisma.plan.findFirstOrThrow({ where: { name: TRIAL_PLAN_NAME}});
return this.prisma.user.create({
diff --git a/lib/services/util.service.ts b/lib/services/util.service.ts
index 2d3d55c..7fb2c11 100644
--- a/lib/services/util.service.ts
+++ b/lib/services/util.service.ts
@@ -7,4 +7,9 @@ export class UtilService {
}
return date;
}
+
+ public static getErrorMessage(error: unknown) {
+ if (error instanceof Error) return error.message
+ return String(error)
+ }
}
\ No newline at end of file
diff --git a/nuxt.config.ts b/nuxt.config.ts
index 4028268..b414d2d 100644
--- a/nuxt.config.ts
+++ b/nuxt.config.ts
@@ -1,5 +1,6 @@
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
+ debug: true,
build: {
transpile: ['trpc-nuxt']
},
@@ -10,4 +11,12 @@ export default defineNuxtConfig({
imports: {
dirs: ['./stores'],
},
+ runtimeConfig:{
+ stripeSecretKey: process.env.STRIPE_SECRET_KEY,
+ stripeEndpointSecret: process.env.STRIPE_ENDPOINT_SECRET,
+ subscriptionGraceDays: 3,
+ public: {
+ debugMode: true,
+ }
+ }
})
diff --git a/package-lock.json b/package-lock.json
index ee728ac..5aa899b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -10,6 +10,8 @@
"@trpc/client": "^10.9.0",
"@trpc/server": "^10.9.0",
"pinia": "^2.0.30",
+ "stripe": "^11.12.0",
+ "superjson": "^1.12.2",
"trpc-nuxt": "^0.5.0",
"zod": "^3.20.2"
},
@@ -1587,6 +1589,11 @@
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.0.tgz",
"integrity": "sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ=="
},
+ "node_modules/@types/node": {
+ "version": "18.14.1",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-18.14.1.tgz",
+ "integrity": "sha512-QH+37Qds3E0eDlReeboBxfHbX9omAcBCXEzswCu6jySP642jiM3cYSIkU/REqwhCUqXdonHFuBfJDiAJxMNhaQ=="
+ },
"node_modules/@types/phoenix": {
"version": "1.5.4",
"resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.5.4.tgz",
@@ -2439,6 +2446,18 @@
"node": ">=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==",
+ "dependencies": {
+ "function-bind": "^1.1.1",
+ "get-intrinsic": "^1.0.2"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/camelcase": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz",
@@ -2811,6 +2830,20 @@
"resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-0.5.0.tgz",
"integrity": "sha512-RyZrFi6PNpBFbIaQjXDlFIhFVqV42QeKSZX1yQIl6ihImq6vcHNGMtqQ/QzY3RMPuYSkvsRwtnt5M9NeYxKt0g=="
},
+ "node_modules/copy-anything": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-3.0.3.tgz",
+ "integrity": "sha512-fpW2W/BqEzqPp29QS+MwwfisHCQZtiduTe/m8idFo0xbti9fIZ2WVhAsCv4ggFVH3AgCkVdpoOCtQC6gBrdhjw==",
+ "dependencies": {
+ "is-what": "^4.1.8"
+ },
+ "engines": {
+ "node": ">=12.13"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/mesqueeb"
+ }
+ },
"node_modules/core-util-is": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
@@ -3756,8 +3789,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/gauge": {
"version": "3.0.2",
@@ -3837,6 +3869,19 @@
"node": "6.* || 8.* || >= 10.*"
}
},
+ "node_modules/get-intrinsic": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.0.tgz",
+ "integrity": "sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==",
+ "dependencies": {
+ "function-bind": "^1.1.1",
+ "has": "^1.0.3",
+ "has-symbols": "^1.0.3"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/get-port-please": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/get-port-please/-/get-port-please-3.0.1.tgz",
@@ -3991,7 +4036,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"
},
@@ -4007,6 +4051,17 @@
"node": ">=4"
}
},
+ "node_modules/has-symbols": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
+ "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/has-unicode": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
@@ -4389,6 +4444,17 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/is-what": {
+ "version": "4.1.8",
+ "resolved": "https://registry.npmjs.org/is-what/-/is-what-4.1.8.tgz",
+ "integrity": "sha512-yq8gMao5upkPoGEU9LsB2P+K3Kt8Q3fQFCGyNCWOAnJAMzEXVV9drYb0TXr42TTliLLhKIBvulgAXgtLLnwzGA==",
+ "engines": {
+ "node": ">=12.13"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/mesqueeb"
+ }
+ },
"node_modules/is-wsl": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz",
@@ -5250,6 +5316,14 @@
"node": ">=0.10.0"
}
},
+ "node_modules/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==",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/ofetch": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/ofetch/-/ofetch-1.0.0.tgz",
@@ -6109,6 +6183,20 @@
"resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz",
"integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw=="
},
+ "node_modules/qs": {
+ "version": "6.11.0",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
+ "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
+ "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",
@@ -6614,6 +6702,19 @@
"node": ">=8"
}
},
+ "node_modules/side-channel": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
+ "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==",
+ "dependencies": {
+ "call-bind": "^1.0.0",
+ "get-intrinsic": "^1.0.2",
+ "object-inspect": "^1.9.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/signal-exit": {
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
@@ -6767,6 +6868,18 @@
"url": "https://github.com/sponsors/antfu"
}
},
+ "node_modules/stripe": {
+ "version": "11.12.0",
+ "resolved": "https://registry.npmjs.org/stripe/-/stripe-11.12.0.tgz",
+ "integrity": "sha512-7yzFyVV/eYpYalfjnw1f9sh/N3r5QVdx5MFtmpOg2QikKVAW4AptXC8P0wj1KNCd/LIo23nTDo0+m9788jHswg==",
+ "dependencies": {
+ "@types/node": ">=8.1.0",
+ "qs": "^6.11.0"
+ },
+ "engines": {
+ "node": ">=12.*"
+ }
+ },
"node_modules/stylehacks": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-5.1.1.tgz",
@@ -6783,6 +6896,17 @@
"postcss": "^8.2.15"
}
},
+ "node_modules/superjson": {
+ "version": "1.12.2",
+ "resolved": "https://registry.npmjs.org/superjson/-/superjson-1.12.2.tgz",
+ "integrity": "sha512-ugvUo9/WmvWOjstornQhsN/sR9mnGtWGYeTxFuqLb4AiT4QdUavjGFRALCPKWWnAiUJ4HTpytj5e0t5HoMRkXg==",
+ "dependencies": {
+ "copy-anything": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/supports-color": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
@@ -9419,6 +9543,11 @@
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.0.tgz",
"integrity": "sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ=="
},
+ "@types/node": {
+ "version": "18.14.1",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-18.14.1.tgz",
+ "integrity": "sha512-QH+37Qds3E0eDlReeboBxfHbX9omAcBCXEzswCu6jySP642jiM3cYSIkU/REqwhCUqXdonHFuBfJDiAJxMNhaQ=="
+ },
"@types/phoenix": {
"version": "1.5.4",
"resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.5.4.tgz",
@@ -10073,6 +10202,15 @@
"integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==",
"dev": true
},
+ "call-bind": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
+ "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==",
+ "requires": {
+ "function-bind": "^1.1.1",
+ "get-intrinsic": "^1.0.2"
+ }
+ },
"camelcase": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz",
@@ -10339,6 +10477,14 @@
"resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-0.5.0.tgz",
"integrity": "sha512-RyZrFi6PNpBFbIaQjXDlFIhFVqV42QeKSZX1yQIl6ihImq6vcHNGMtqQ/QzY3RMPuYSkvsRwtnt5M9NeYxKt0g=="
},
+ "copy-anything": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-3.0.3.tgz",
+ "integrity": "sha512-fpW2W/BqEzqPp29QS+MwwfisHCQZtiduTe/m8idFo0xbti9fIZ2WVhAsCv4ggFVH3AgCkVdpoOCtQC6gBrdhjw==",
+ "requires": {
+ "is-what": "^4.1.8"
+ }
+ },
"core-util-is": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
@@ -11037,8 +11183,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=="
},
"gauge": {
"version": "3.0.2",
@@ -11102,6 +11247,16 @@
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
"dev": true
},
+ "get-intrinsic": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.0.tgz",
+ "integrity": "sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==",
+ "requires": {
+ "function-bind": "^1.1.1",
+ "has": "^1.0.3",
+ "has-symbols": "^1.0.3"
+ }
+ },
"get-port-please": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/get-port-please/-/get-port-please-3.0.1.tgz",
@@ -11220,7 +11375,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"
}
@@ -11230,6 +11384,11 @@
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
"integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="
},
+ "has-symbols": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
+ "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A=="
+ },
"has-unicode": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
@@ -11500,6 +11659,11 @@
"integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==",
"dev": true
},
+ "is-what": {
+ "version": "4.1.8",
+ "resolved": "https://registry.npmjs.org/is-what/-/is-what-4.1.8.tgz",
+ "integrity": "sha512-yq8gMao5upkPoGEU9LsB2P+K3Kt8Q3fQFCGyNCWOAnJAMzEXVV9drYb0TXr42TTliLLhKIBvulgAXgtLLnwzGA=="
+ },
"is-wsl": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz",
@@ -12187,6 +12351,11 @@
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
"dev": true
},
+ "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=="
+ },
"ofetch": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/ofetch/-/ofetch-1.0.0.tgz",
@@ -12764,6 +12933,14 @@
"resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz",
"integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw=="
},
+ "qs": {
+ "version": "6.11.0",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
+ "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
+ "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",
@@ -13132,6 +13309,16 @@
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
"dev": true
},
+ "side-channel": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
+ "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==",
+ "requires": {
+ "call-bind": "^1.0.0",
+ "get-intrinsic": "^1.0.2",
+ "object-inspect": "^1.9.0"
+ }
+ },
"signal-exit": {
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
@@ -13249,6 +13436,15 @@
"acorn": "^8.8.1"
}
},
+ "stripe": {
+ "version": "11.12.0",
+ "resolved": "https://registry.npmjs.org/stripe/-/stripe-11.12.0.tgz",
+ "integrity": "sha512-7yzFyVV/eYpYalfjnw1f9sh/N3r5QVdx5MFtmpOg2QikKVAW4AptXC8P0wj1KNCd/LIo23nTDo0+m9788jHswg==",
+ "requires": {
+ "@types/node": ">=8.1.0",
+ "qs": "^6.11.0"
+ }
+ },
"stylehacks": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-5.1.1.tgz",
@@ -13259,6 +13455,14 @@
"postcss-selector-parser": "^6.0.4"
}
},
+ "superjson": {
+ "version": "1.12.2",
+ "resolved": "https://registry.npmjs.org/superjson/-/superjson-1.12.2.tgz",
+ "integrity": "sha512-ugvUo9/WmvWOjstornQhsN/sR9mnGtWGYeTxFuqLb4AiT4QdUavjGFRALCPKWWnAiUJ4HTpytj5e0t5HoMRkXg==",
+ "requires": {
+ "copy-anything": "^3.0.2"
+ }
+ },
"supports-color": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
diff --git a/package.json b/package.json
index deeb2fb..0fbd18e 100644
--- a/package.json
+++ b/package.json
@@ -18,6 +18,8 @@
"@trpc/client": "^10.9.0",
"@trpc/server": "^10.9.0",
"pinia": "^2.0.30",
+ "stripe": "^11.12.0",
+ "superjson": "^1.12.2",
"trpc-nuxt": "^0.5.0",
"zod": "^3.20.2"
},
diff --git a/pages/account.vue b/pages/account.vue
new file mode 100644
index 0000000..c91a91f
--- /dev/null
+++ b/pages/account.vue
@@ -0,0 +1,31 @@
+
+
+
+
Account
+
Name: {{ activeMembership?.account.name }}
+
Current Period Ends: {{ formatDate(activeMembership?.account.current_period_ends) }}
+
Permitted Features: {{ activeMembership?.account.features }}
+
Maximum Notes: {{ activeMembership?.account.max_notes }}
+
Access Level: {{ activeMembership?.access }}
+
+
+ ******* Debug *******
+ Account ID: {{ activeMembership?.account.id }}
+ Plan Id: {{ activeMembership?.account.plan_id }}
+ Stripe Subscription Id: {{ activeMembership?.account.stripe_subscription_id }}
+ Stripe Customer Id: {{ activeMembership?.account.stripe_customer_id }}
+ Membership Id: {{ activeMembership?.id }}
+ User Id: {{ activeMembership?.user_id }}
+
+
+
diff --git a/pages/cancel.vue b/pages/cancel.vue
new file mode 100644
index 0000000..e5cd1ed
--- /dev/null
+++ b/pages/cancel.vue
@@ -0,0 +1,9 @@
+
+
+
+ We are sorry that you canceled your transaction!
+ Pricing
+ To Your Dashboard
+
+
+
\ No newline at end of file
diff --git a/pages/dashboard.vue b/pages/dashboard.vue
index 714c172..62ad8ce 100644
--- a/pages/dashboard.vue
+++ b/pages/dashboard.vue
@@ -8,9 +8,6 @@
const store = useAppStore();
const { notes } = storeToRefs(store); // ensure the notes list is reactive
- onMounted(async () => {
- await store.initUser();
- })
diff --git a/pages/fail.vue b/pages/fail.vue
new file mode 100644
index 0000000..1c7912f
--- /dev/null
+++ b/pages/fail.vue
@@ -0,0 +1,9 @@
+
+
+
+ We are sorry that you were unable to subscribe.
+ Pricing
+ To Your Dashboard
+
+
+
\ No newline at end of file
diff --git a/pages/pricing.vue b/pages/pricing.vue
new file mode 100644
index 0000000..d435424
--- /dev/null
+++ b/pages/pricing.vue
@@ -0,0 +1,28 @@
+
+
+
+
Pricing
+
+
+
+
+
diff --git a/pages/success.vue b/pages/success.vue
new file mode 100644
index 0000000..80ccdf3
--- /dev/null
+++ b/pages/success.vue
@@ -0,0 +1,23 @@
+
+
+
+
+
+ We appreciate your business {{customer.name}}!
+ It appears your stripe customer information has been deleted!
+
+
To Your Dashboard
+
+
\ No newline at end of file
diff --git a/plugins/trpcClient.ts b/plugins/trpcClient.ts
index 62c7d59..0f3254e 100644
--- a/plugins/trpcClient.ts
+++ b/plugins/trpcClient.ts
@@ -1,5 +1,6 @@
import { createTRPCNuxtClient, httpBatchLink } from 'trpc-nuxt/client'
import type { AppRouter } from '~/server/api/trpc/[trpc]'
+import superjson from 'superjson';
export default defineNuxtPlugin(() => {
/**
@@ -12,6 +13,7 @@ export default defineNuxtPlugin(() => {
url: '/api/trpc',
}),
],
+ transformer: superjson,
})
return {
diff --git a/prisma/prisma.client.ts b/prisma/prisma.client.ts
new file mode 100644
index 0000000..c1ce66a
--- /dev/null
+++ b/prisma/prisma.client.ts
@@ -0,0 +1,5 @@
+import pkg from "@prisma/client";
+
+const { PrismaClient } = pkg;
+const prisma_client = new PrismaClient()
+export default prisma_client
\ No newline at end of file
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index 8c6d269..57ffeb8 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -49,6 +49,8 @@ model Account {
members Membership[]
notes Note[]
max_notes Int @default(100)
+ stripe_subscription_id String?
+ stripe_customer_id String?
@@map("account")
}
diff --git a/server/routes/create-checkout-session.post.ts b/server/routes/create-checkout-session.post.ts
new file mode 100644
index 0000000..c24ab71
--- /dev/null
+++ b/server/routes/create-checkout-session.post.ts
@@ -0,0 +1,54 @@
+import { Account } from '@prisma/client';
+import Stripe from 'stripe';
+import UserAccountService from '~~/lib/services/user.account.service';
+import prisma_client from '~~/prisma/prisma.client';
+
+const config = useRuntimeConfig();
+const stripe = new Stripe(config.stripeSecretKey, { apiVersion: '2022-11-15' });
+
+export default defineEventHandler(async (event) => {
+ const YOUR_DOMAIN = 'http://localhost:3000'; // TODO - pull from somewhere, this is shit
+
+ const body = await readBody(event)
+ let { price_id, account_id} = body;
+ account_id = +account_id
+ console.log(`session.post.ts recieved price_id:${price_id}, account_id:${account_id}`);
+
+ const userService = new UserAccountService(prisma_client);
+ const account: Account = await userService.getAccountById(account_id);
+ let customer_id: string
+ if(!account.stripe_customer_id){
+ // need to pre-emptively create a Stripe user for this account (use name for now, just so is visible on dashboard) TODO - include Email
+ console.log(`Creating account with name ${account.name}`);
+ const customer = await stripe.customers.create({ name: account.name });
+ customer_id = customer.id;
+ userService.updateAccountStipeCustomerId(account_id, customer.id);
+ } else {
+ customer_id = account.stripe_customer_id;
+ }
+
+ const session = await stripe.checkout.sessions.create({
+ mode: 'subscription',
+ line_items: [
+ {
+ price: price_id,
+ // For metered billing, do not pass quantity
+ quantity: 1,
+ },
+ ],
+ // {CHECKOUT_SESSION_ID} is a string literal; do not change it!
+ // the actual Session ID is returned in the query parameter when your customer
+ // is redirected to the success page.
+ success_url: `${YOUR_DOMAIN}/success?session_id={CHECKOUT_SESSION_ID}`,
+ cancel_url: `${YOUR_DOMAIN}/cancel`,
+ customer: customer_id
+ });
+
+ if(session?.url){
+ return sendRedirect(event, session.url, 303);
+ } else {
+ return sendRedirect(event, `${YOUR_DOMAIN}/fail`, 303);
+ }
+});
+
+
diff --git a/server/routes/webhook.post.ts b/server/routes/webhook.post.ts
new file mode 100644
index 0000000..cdefa6a
--- /dev/null
+++ b/server/routes/webhook.post.ts
@@ -0,0 +1,42 @@
+import Stripe from 'stripe';
+import UserAccountService from '~~/lib/services/user.account.service';
+import prisma_client from '~~/prisma/prisma.client';
+
+const config = useRuntimeConfig();
+const stripe = new Stripe(config.stripeSecretKey, { apiVersion: '2022-11-15' });
+
+export default defineEventHandler(async (event) => {
+ const stripeSignature = getRequestHeader(event, 'stripe-signature');
+ if(!stripeSignature){
+ throw createError({ statusCode: 400, statusMessage: 'Webhook Error: No stripe signature in header' });
+ }
+
+ const rawBody = await readRawBody(event)
+ if(!rawBody){
+ throw createError({ statusCode: 400, statusMessage: 'Webhook Error: No body' });
+ }
+ let stripeEvent: Stripe.Event;
+
+ try {
+ stripeEvent = stripe.webhooks.constructEvent(rawBody, stripeSignature, config.stripeEndpointSecret);
+ }
+ catch (err) {
+ console.log(err);
+ throw createError({ statusCode: 400, statusMessage: `Webhook Error` }); // ${(err as Error).message}
+ }
+
+ console.log(`****** Web Hook Recieved (${stripeEvent.type}) ******`);
+
+ if(stripeEvent.type && stripeEvent.type.startsWith('customer.subscription')){
+ let subscription = stripeEvent.data.object as Stripe.Subscription;
+
+ const userService = new UserAccountService(prisma_client);
+
+ let current_period_ends: Date = new Date(subscription.current_period_end * 1000);
+ current_period_ends.setDate(current_period_ends.getDate() + config.subscriptionGraceDays);
+
+ console.log(`updating stripe sub details subscription.current_period_end:${subscription.current_period_end}, subscription.id:${subscription.id}`);
+ userService.updateStripeSubscriptionDetailsForAccount(subscription.customer.toString(), subscription.id, current_period_ends)
+ }
+ return `handled ${stripeEvent.type}.`;
+});
\ No newline at end of file
diff --git a/server/trpc/context.ts b/server/trpc/context.ts
index 088d3b1..57b2f67 100644
--- a/server/trpc/context.ts
+++ b/server/trpc/context.ts
@@ -33,9 +33,10 @@ export async function createContext(event: H3Event){
}
if(!supabase || !user || !prisma || !dbUser) {
+
throw new TRPCError({
code: 'INTERNAL_SERVER_ERROR',
- message: 'Unable to fetch user data, please try again later.',
+ message: `Unable to fetch user data, please try again later. Missing ->[supabase:${(!supabase)},user:${(!user)},prisma:${(!prisma)},dbUser:${(!dbUser)}, ]`,
});
}
diff --git a/server/trpc/trpc.ts b/server/trpc/trpc.ts
index 963939c..1fbe75e 100644
--- a/server/trpc/trpc.ts
+++ b/server/trpc/trpc.ts
@@ -11,8 +11,11 @@ import { initTRPC, TRPCError } from '@trpc/server'
import { Context } from './context';
import { z } from 'zod';
import { ACCOUNT_ACCESS } from '@prisma/client';
+import superjson from 'superjson';
-const t = initTRPC.context
().create()
+const t = initTRPC.context().create({
+ transformer: superjson,
+})
/**
* auth middlewares
diff --git a/stores/app.store.ts b/stores/app.store.ts
index b9b9f22..c1c6bcb 100644
--- a/stores/app.store.ts
+++ b/stores/app.store.ts
@@ -17,13 +17,16 @@ export const useAppStore = defineStore('app', {
},
actions: {
async initUser() {
- const { $client } = useNuxtApp();
- const { data: dbUser } = await $client.userAccount.getDBUser.useQuery();
- if(dbUser.value?.dbUser){
- this.dbUser = dbUser.value.dbUser;
- if(dbUser.value.dbUser.memberships.length > 0){
- this.activeMembership = dbUser.value.dbUser.memberships[0];
- await this.fetchNotesForCurrentUser();
+ if(!this.dbUser || !this.activeMembership){
+ const { $client } = useNuxtApp();
+ const { dbUser } = await $client.userAccount.getDBUser.query();
+
+ if(dbUser){
+ this.dbUser = dbUser;
+ if(dbUser.memberships.length > 0){
+ this.activeMembership = dbUser.memberships[0];
+ await this.fetchNotesForCurrentUser();
+ }
}
}
},
@@ -31,9 +34,9 @@ export const useAppStore = defineStore('app', {
if(!this.activeMembership) { return; }
const { $client } = useNuxtApp();
- const { data: foundNotes } = await $client.notes.getForCurrentUser.useQuery({account_id: this.activeMembership.account_id});
- if(foundNotes.value?.notes){
- this.notes = foundNotes.value.notes;
+ const { notes } = await $client.notes.getForCurrentUser.query({account_id: this.activeMembership.account_id});
+ if(notes){
+ this.notes = notes;
}
},
async changeActiveMembership(membership: MembershipWithAccount) {