Basic Stripe integration. new pages for stripe handshake and new Account page. use query instead of useQuery for trpc fetches from store
This commit is contained in:
@@ -103,7 +103,9 @@ npx prisma generate
|
||||
- actions which mutate the current user account should update the context... (done)
|
||||
- integration with stripe including web hooks.
|
||||
-- add email to user record... capture from login same as user name
|
||||
-- add a pricing page....should be the default redirect from signup if the user has no active plan.. not sure whether to use a 'blank' plan or make plan nullable
|
||||
-- initial user should be created with an expired plan
|
||||
-- add a pricing page....should be the default redirect from signup if the user has no active plan.. not sure whether to use a 'blank' plan or make plan nullable (basic pricing page is done)
|
||||
-- figure out what to do with Plan Name. Could add Plan Name to account record and copy over at time of account creation or updation. could pull from the Plan record for display.... but makes it difficult to change... should be loosely coupled, maybe use first approach
|
||||
|
||||
# Admin Functions Scenario (shitty test)
|
||||
Pre-condition
|
||||
|
||||
@@ -1,20 +1,24 @@
|
||||
<script setup lang="ts">
|
||||
import { MembershipWithAccount } from '~~/lib/services/user.account.service';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { MembershipWithAccount } from '~~/lib/services/user.account.service';
|
||||
import { storeToRefs } from 'pinia';
|
||||
|
||||
const supabase = useSupabaseAuthClient();
|
||||
const user = useSupabaseUser();
|
||||
const store = useAppStore()
|
||||
const { activeMembership } = storeToRefs(store);
|
||||
const supabase = useSupabaseAuthClient();
|
||||
const user = useSupabaseUser();
|
||||
const store = useAppStore()
|
||||
const { activeMembership } = storeToRefs(store);
|
||||
|
||||
const { $client } = useNuxtApp();
|
||||
const { $client } = useNuxtApp();
|
||||
|
||||
const { data: dbUser } = await $client.userAccount.getDBUser.useQuery();
|
||||
const { data: dbUser } = await $client.userAccount.getDBUser.useQuery();
|
||||
|
||||
async function signout() {
|
||||
await supabase.auth.signOut();
|
||||
navigateTo('/', {replace: true});
|
||||
}
|
||||
onMounted(async () => {
|
||||
await store.initUser();
|
||||
});
|
||||
|
||||
async function signout() {
|
||||
await supabase.auth.signOut();
|
||||
navigateTo('/', {replace: true});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -23,6 +27,12 @@ async function signout() {
|
||||
<!-- logged in & sign out -->
|
||||
<div v-if="user">logged in as: {{ user.email }}: <button @click="signout()">Sign Out</button></div>
|
||||
<div v-if="!user">Not Logged in</div>
|
||||
<div>
|
||||
<NuxtLink to="/">Boilerplate</NuxtLink>
|
||||
| <NuxtLink to="/dashboard">Dashboard</NuxtLink>
|
||||
| <NuxtLink to="/pricing">Pricing</NuxtLink>
|
||||
| <NuxtLink to="/account">Account</NuxtLink>
|
||||
</div>
|
||||
|
||||
<!-- Account Switching -->
|
||||
<p v-if="(dbUser?.dbUser?.memberships) && (dbUser.dbUser.memberships.length > 0)">
|
||||
|
||||
@@ -40,6 +40,34 @@ export default class UserAccountService {
|
||||
});
|
||||
}
|
||||
|
||||
async getAccountById(account_id: number): Promise<Account> {
|
||||
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<FullDBUser | null> {
|
||||
const trialPlan = await this.prisma.plan.findFirstOrThrow({ where: { name: TRIAL_PLAN_NAME}});
|
||||
return this.prisma.user.create({
|
||||
|
||||
@@ -7,4 +7,9 @@ export class UtilService {
|
||||
}
|
||||
return date;
|
||||
}
|
||||
|
||||
public static getErrorMessage(error: unknown) {
|
||||
if (error instanceof Error) return error.message
|
||||
return String(error)
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
216
package-lock.json
generated
216
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
31
pages/account.vue
Normal file
31
pages/account.vue
Normal file
@@ -0,0 +1,31 @@
|
||||
<script setup lang="ts">
|
||||
import { storeToRefs } from 'pinia';
|
||||
const store = useAppStore();
|
||||
const { activeMembership } = storeToRefs(store);
|
||||
const config = useRuntimeConfig();
|
||||
|
||||
function formatDate(date: Date | undefined){
|
||||
if(!date){ return ""; }
|
||||
return new Intl.DateTimeFormat('default', {dateStyle: 'long'}).format(date);
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<div>
|
||||
<h3>Account</h3>
|
||||
<p>Name: {{ activeMembership?.account.name }}</p>
|
||||
<p>Current Period Ends: {{ formatDate(activeMembership?.account.current_period_ends) }}</p>
|
||||
<p>Permitted Features: {{ activeMembership?.account.features }}</p>
|
||||
<p>Maximum Notes: {{ activeMembership?.account.max_notes }}</p>
|
||||
<p>Access Level: {{ activeMembership?.access }}</p>
|
||||
|
||||
<template v-if="config.public.debugMode">
|
||||
<p>******* Debug *******</p>
|
||||
<p>Account ID: {{ activeMembership?.account.id }}</p>
|
||||
<p>Plan Id: {{ activeMembership?.account.plan_id }}</p>
|
||||
<p>Stripe Subscription Id: {{ activeMembership?.account.stripe_subscription_id }}</p>
|
||||
<p>Stripe Customer Id: {{ activeMembership?.account.stripe_customer_id }}</p>
|
||||
<p>Membership Id: {{ activeMembership?.id }}</p>
|
||||
<p>User Id: {{ activeMembership?.user_id }}</p>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
9
pages/cancel.vue
Normal file
9
pages/cancel.vue
Normal file
@@ -0,0 +1,9 @@
|
||||
<template>
|
||||
<div>
|
||||
<p>
|
||||
We are sorry that you canceled your transaction!
|
||||
<NuxtLink to="/pricing">Pricing</NuxtLink>
|
||||
<NuxtLink to="/dashboard">To Your Dashboard</NuxtLink>
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
@@ -8,9 +8,6 @@
|
||||
const store = useAppStore();
|
||||
const { notes } = storeToRefs(store); // ensure the notes list is reactive
|
||||
|
||||
onMounted(async () => {
|
||||
await store.initUser();
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<div>
|
||||
|
||||
9
pages/fail.vue
Normal file
9
pages/fail.vue
Normal file
@@ -0,0 +1,9 @@
|
||||
<template>
|
||||
<div>
|
||||
<p>
|
||||
We are sorry that you were unable to subscribe.
|
||||
<NuxtLink to="/pricing">Pricing</NuxtLink>
|
||||
<NuxtLink to="/dashboard">To Your Dashboard</NuxtLink>
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
28
pages/pricing.vue
Normal file
28
pages/pricing.vue
Normal file
@@ -0,0 +1,28 @@
|
||||
<script setup lang="ts">
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { ACCOUNT_ACCESS } from '@prisma/client';
|
||||
|
||||
const store = useAppStore()
|
||||
const { activeMembership } = storeToRefs(store);
|
||||
|
||||
</script>
|
||||
<template>
|
||||
<div>
|
||||
<h3>Pricing</h3>
|
||||
<form action="/create-checkout-session" method="POST">
|
||||
<label for="submit">Individual Plan, Normal Price</label>
|
||||
<input type="hidden" name="price_id" value="price_1MfaKVJfLn4RhYiLgruKo89E" />
|
||||
<input type="hidden" name="account_id" :value="activeMembership?.account_id" />
|
||||
|
||||
<button type="submit" :disabled="!activeMembership || (activeMembership.access !== ACCOUNT_ACCESS.OWNER && activeMembership.access !== ACCOUNT_ACCESS.ADMIN)">Checkout</button>
|
||||
</form>
|
||||
|
||||
<form action="/create-checkout-session" method="POST">
|
||||
<label for="submit">Team Plan, Normal Price</label>
|
||||
<input type="hidden" name="price_id" value="price_1MfaM6JfLn4RhYiLPdr1OTDS" />
|
||||
<input type="hidden" name="account_id" :value="activeMembership?.account_id" />
|
||||
|
||||
<button type="submit" :disabled="!activeMembership || (activeMembership.access !== ACCOUNT_ACCESS.OWNER && activeMembership.access !== ACCOUNT_ACCESS.ADMIN)">Checkout</button>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
23
pages/success.vue
Normal file
23
pages/success.vue
Normal file
@@ -0,0 +1,23 @@
|
||||
<script setup lang="ts">
|
||||
import Stripe from 'stripe';
|
||||
const config = useRuntimeConfig();
|
||||
const stripe = new Stripe(config.stripeSecretKey, { apiVersion: '2022-11-15' });
|
||||
const route = useRoute();
|
||||
let customer: Stripe.Response<Stripe.Customer | Stripe.DeletedCustomer>
|
||||
try{
|
||||
const session = await stripe.checkout.sessions.retrieve(route?.query?.session_id as string);
|
||||
customer = await stripe.customers.retrieve(session?.customer as string);
|
||||
} catch(e) {
|
||||
console.log(`Error ${e}`)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<p>
|
||||
<span v-if="!customer.deleted">We appreciate your business {{customer.name}}!</span>
|
||||
<span v-if="customer.deleted">It appears your stripe customer information has been deleted!</span>
|
||||
</p>
|
||||
<p><NuxtLink to="/dashboard">To Your Dashboard</NuxtLink></p>
|
||||
</div>
|
||||
</template>
|
||||
@@ -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 {
|
||||
|
||||
5
prisma/prisma.client.ts
Normal file
5
prisma/prisma.client.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import pkg from "@prisma/client";
|
||||
|
||||
const { PrismaClient } = pkg;
|
||||
const prisma_client = new PrismaClient()
|
||||
export default prisma_client
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
54
server/routes/create-checkout-session.post.ts
Normal file
54
server/routes/create-checkout-session.post.ts
Normal file
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
42
server/routes/webhook.post.ts
Normal file
42
server/routes/webhook.post.ts
Normal file
@@ -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}.`;
|
||||
});
|
||||
@@ -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)}, ]`,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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<Context>().create()
|
||||
const t = initTRPC.context<Context>().create({
|
||||
transformer: superjson,
|
||||
})
|
||||
|
||||
/**
|
||||
* auth middlewares
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user