Merge pull request #2 from MarconLP/feature/posthog-event-tracking
Feature/posthog event tracking
This commit is contained in:
commit
8ae1fce9d8
20 changed files with 559 additions and 148 deletions
54
package-lock.json
generated
54
package-lock.json
generated
|
|
@ -34,8 +34,9 @@
|
||||||
"micro": "^10.0.1",
|
"micro": "^10.0.1",
|
||||||
"micro-cors": "^0.1.1",
|
"micro-cors": "^0.1.1",
|
||||||
"next": "^13.3.0",
|
"next": "^13.3.0",
|
||||||
"next-auth": "^4.21.0",
|
"next-auth": "^4.22.1",
|
||||||
"posthog-js": "^1.53.4",
|
"posthog-js": "^1.53.4",
|
||||||
|
"posthog-node": "^3.1.0",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
"react-media-recorder": "^1.6.6",
|
"react-media-recorder": "^1.6.6",
|
||||||
|
|
@ -5349,9 +5350,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/next-auth": {
|
"node_modules/next-auth": {
|
||||||
"version": "4.22.0",
|
"version": "4.22.1",
|
||||||
"resolved": "https://registry.npmjs.org/next-auth/-/next-auth-4.22.0.tgz",
|
"resolved": "https://registry.npmjs.org/next-auth/-/next-auth-4.22.1.tgz",
|
||||||
"integrity": "sha512-08+kjnDoE7aQ52O996x6cwA3ffc2CbHIkrCgLYhbE+aDIJBKI0oA9UbIEIe19/+ODYJgpAHHOtJx4izmsgaVag==",
|
"integrity": "sha512-NTR3f6W7/AWXKw8GSsgSyQcDW6jkslZLH8AiZa5PQ09w1kR8uHtR9rez/E9gAq/o17+p0JYHE8QjF3RoniiObA==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.20.13",
|
"@babel/runtime": "^7.20.13",
|
||||||
"@panva/hkdf": "^1.0.2",
|
"@panva/hkdf": "^1.0.2",
|
||||||
|
|
@ -5858,6 +5859,26 @@
|
||||||
"rrweb-snapshot": "^1.1.14"
|
"rrweb-snapshot": "^1.1.14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/posthog-node": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/posthog-node/-/posthog-node-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-hXhkDWigzNYgkfLpd3HbfCD0h1/zP19pPXEba2Daf6xCerrFxc7ixMDtXwCQMXOmJNvrcoVMvrjULpfKHN11vA==",
|
||||||
|
"dependencies": {
|
||||||
|
"axios": "^0.27.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=15.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/posthog-node/node_modules/axios": {
|
||||||
|
"version": "0.27.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz",
|
||||||
|
"integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"follow-redirects": "^1.14.9",
|
||||||
|
"form-data": "^4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/preact": {
|
"node_modules/preact": {
|
||||||
"version": "10.13.2",
|
"version": "10.13.2",
|
||||||
"resolved": "https://registry.npmjs.org/preact/-/preact-10.13.2.tgz",
|
"resolved": "https://registry.npmjs.org/preact/-/preact-10.13.2.tgz",
|
||||||
|
|
@ -11126,9 +11147,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"next-auth": {
|
"next-auth": {
|
||||||
"version": "4.22.0",
|
"version": "4.22.1",
|
||||||
"resolved": "https://registry.npmjs.org/next-auth/-/next-auth-4.22.0.tgz",
|
"resolved": "https://registry.npmjs.org/next-auth/-/next-auth-4.22.1.tgz",
|
||||||
"integrity": "sha512-08+kjnDoE7aQ52O996x6cwA3ffc2CbHIkrCgLYhbE+aDIJBKI0oA9UbIEIe19/+ODYJgpAHHOtJx4izmsgaVag==",
|
"integrity": "sha512-NTR3f6W7/AWXKw8GSsgSyQcDW6jkslZLH8AiZa5PQ09w1kR8uHtR9rez/E9gAq/o17+p0JYHE8QjF3RoniiObA==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@babel/runtime": "^7.20.13",
|
"@babel/runtime": "^7.20.13",
|
||||||
"@panva/hkdf": "^1.0.2",
|
"@panva/hkdf": "^1.0.2",
|
||||||
|
|
@ -11451,6 +11472,25 @@
|
||||||
"rrweb-snapshot": "^1.1.14"
|
"rrweb-snapshot": "^1.1.14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"posthog-node": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/posthog-node/-/posthog-node-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-hXhkDWigzNYgkfLpd3HbfCD0h1/zP19pPXEba2Daf6xCerrFxc7ixMDtXwCQMXOmJNvrcoVMvrjULpfKHN11vA==",
|
||||||
|
"requires": {
|
||||||
|
"axios": "^0.27.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"axios": {
|
||||||
|
"version": "0.27.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz",
|
||||||
|
"integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==",
|
||||||
|
"requires": {
|
||||||
|
"follow-redirects": "^1.14.9",
|
||||||
|
"form-data": "^4.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"preact": {
|
"preact": {
|
||||||
"version": "10.13.2",
|
"version": "10.13.2",
|
||||||
"resolved": "https://registry.npmjs.org/preact/-/preact-10.13.2.tgz",
|
"resolved": "https://registry.npmjs.org/preact/-/preact-10.13.2.tgz",
|
||||||
|
|
|
||||||
|
|
@ -42,8 +42,9 @@
|
||||||
"micro": "^10.0.1",
|
"micro": "^10.0.1",
|
||||||
"micro-cors": "^0.1.1",
|
"micro-cors": "^0.1.1",
|
||||||
"next": "^13.3.0",
|
"next": "^13.3.0",
|
||||||
"next-auth": "^4.21.0",
|
"next-auth": "^4.22.1",
|
||||||
"posthog-js": "^1.53.4",
|
"posthog-js": "^1.53.4",
|
||||||
|
"posthog-node": "^3.1.0",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
"react-media-recorder": "^1.6.6",
|
"react-media-recorder": "^1.6.6",
|
||||||
|
|
|
||||||
|
|
@ -5,22 +5,36 @@ import { useAtom } from "jotai";
|
||||||
import recordVideoModalOpen from "~/atoms/recordVideoModalOpen";
|
import recordVideoModalOpen from "~/atoms/recordVideoModalOpen";
|
||||||
import paywallAtom from "~/atoms/paywallAtom";
|
import paywallAtom from "~/atoms/paywallAtom";
|
||||||
import { useSession } from "next-auth/react";
|
import { useSession } from "next-auth/react";
|
||||||
|
import { usePostHog } from "posthog-js/react";
|
||||||
|
|
||||||
export default function NewVideoMenu() {
|
export default function NewVideoMenu() {
|
||||||
const [, setRecordOpen] = useAtom(recordVideoModalOpen);
|
const [, setRecordOpen] = useAtom(recordVideoModalOpen);
|
||||||
const [, setUploadOpen] = useAtom(uploadVideoModalOpen);
|
const [, setUploadOpen] = useAtom(uploadVideoModalOpen);
|
||||||
const [, setPaywallOpen] = useAtom(paywallAtom);
|
const [, setPaywallOpen] = useAtom(paywallAtom);
|
||||||
const { data: session } = useSession();
|
const { data: session } = useSession();
|
||||||
|
const posthog = usePostHog();
|
||||||
|
|
||||||
const openRecordModal = () => {
|
const openRecordModal = () => {
|
||||||
setRecordOpen(true);
|
setRecordOpen(true);
|
||||||
|
|
||||||
|
posthog?.capture("open record video modal", {
|
||||||
|
stripeSubscriptionStatus: session?.user.stripeSubscriptionStatus,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const openUploadModal = () => {
|
const openUploadModal = () => {
|
||||||
if (session?.user.stripeSubscriptionStatus === "active") {
|
if (session?.user.stripeSubscriptionStatus === "active") {
|
||||||
setUploadOpen(true);
|
setUploadOpen(true);
|
||||||
|
|
||||||
|
posthog?.capture("open upload video modal", {
|
||||||
|
stripeSubscriptionStatus: session?.user.stripeSubscriptionStatus,
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
setPaywallOpen(true);
|
setPaywallOpen(true);
|
||||||
|
|
||||||
|
posthog?.capture("hit video upload paywall", {
|
||||||
|
stripeSubscriptionStatus: session?.user.stripeSubscriptionStatus,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import { api } from "~/utils/api";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { CheckIcon } from "@heroicons/react/20/solid";
|
import { CheckIcon } from "@heroicons/react/20/solid";
|
||||||
import Tooltip from "~/components/Tooltip";
|
import Tooltip from "~/components/Tooltip";
|
||||||
|
import { usePostHog } from "posthog-js/react";
|
||||||
|
|
||||||
export default function Paywall() {
|
export default function Paywall() {
|
||||||
const { mutateAsync: createCheckoutSession } =
|
const { mutateAsync: createCheckoutSession } =
|
||||||
|
|
@ -14,9 +15,12 @@ export default function Paywall() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [open, setOpen] = useAtom(paywallAtom);
|
const [open, setOpen] = useAtom(paywallAtom);
|
||||||
const [billedAnnually, setBilledAnnually] = useState<boolean>(false);
|
const [billedAnnually, setBilledAnnually] = useState<boolean>(false);
|
||||||
|
const posthog = usePostHog();
|
||||||
|
|
||||||
function closeModal() {
|
function closeModal() {
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
|
|
||||||
|
posthog?.capture("close paywall");
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleCheckout = async () => {
|
const handleCheckout = async () => {
|
||||||
|
|
@ -26,6 +30,12 @@ export default function Paywall() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const toggleBillingCycle = () => {
|
||||||
|
setBilledAnnually(!billedAnnually);
|
||||||
|
|
||||||
|
posthog?.capture("change billing cycle");
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Transition appear show={open} as={Fragment}>
|
<Transition appear show={open} as={Fragment}>
|
||||||
<Dialog as="div" className="relative z-10" onClose={closeModal}>
|
<Dialog as="div" className="relative z-10" onClose={closeModal}>
|
||||||
|
|
@ -85,7 +95,7 @@ export default function Paywall() {
|
||||||
<span className=" svelte-10wstod">Monthly</span>
|
<span className=" svelte-10wstod">Monthly</span>
|
||||||
<label className="group relative flex cursor-pointer items-center gap-2 text-sm font-medium text-gray-600">
|
<label className="group relative flex cursor-pointer items-center gap-2 text-sm font-medium text-gray-600">
|
||||||
<input
|
<input
|
||||||
onChange={() => setBilledAnnually(!billedAnnually)}
|
onChange={toggleBillingCycle}
|
||||||
checked={billedAnnually}
|
checked={billedAnnually}
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
className="peer absolute left-1/2 hidden h-full w-full -translate-x-1/2 rounded-md"
|
className="peer absolute left-1/2 hidden h-full w-full -translate-x-1/2 rounded-md"
|
||||||
|
|
|
||||||
|
|
@ -3,12 +3,26 @@ import { Fragment } from "react";
|
||||||
import { signOut, useSession } from "next-auth/react";
|
import { signOut, useSession } from "next-auth/react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { api } from "~/utils/api";
|
import { api } from "~/utils/api";
|
||||||
|
import { usePostHog } from "posthog-js/react";
|
||||||
|
|
||||||
export default function ProfileMenu() {
|
export default function ProfileMenu() {
|
||||||
const { mutateAsync: createBillingPortalSession } =
|
const { mutateAsync: createBillingPortalSession } =
|
||||||
api.stripe.createBillingPortalSession.useMutation();
|
api.stripe.createBillingPortalSession.useMutation();
|
||||||
const { push } = useRouter();
|
const { push } = useRouter();
|
||||||
const { data: session } = useSession();
|
const { data: session } = useSession();
|
||||||
|
const posthog = usePostHog();
|
||||||
|
|
||||||
|
const openBillingSettings = () => {
|
||||||
|
void createBillingPortalSession().then(({ billingPortalUrl }) => {
|
||||||
|
if (billingPortalUrl) {
|
||||||
|
void push(billingPortalUrl);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
posthog?.capture("billing settings opened", {
|
||||||
|
stripeSubscriptionStatus: session?.user.stripeSubscriptionStatus,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Menu as="div" className="relative inline-block text-left">
|
<Menu as="div" className="relative inline-block text-left">
|
||||||
|
|
@ -34,15 +48,7 @@ export default function ProfileMenu() {
|
||||||
<Menu.Item>
|
<Menu.Item>
|
||||||
{({ active }) => (
|
{({ active }) => (
|
||||||
<div
|
<div
|
||||||
onClick={() => {
|
onClick={openBillingSettings}
|
||||||
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 ${
|
className={`mx-2 flex h-8 w-40 cursor-pointer flex-row content-center rounded-md p-2 ${
|
||||||
active ? "bg-gray-100" : ""
|
active ? "bg-gray-100" : ""
|
||||||
}`}
|
}`}
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ import { TRPCClientError } from "@trpc/client";
|
||||||
import { useAtom } from "jotai/index";
|
import { useAtom } from "jotai/index";
|
||||||
import paywallAtom from "~/atoms/paywallAtom";
|
import paywallAtom from "~/atoms/paywallAtom";
|
||||||
import recordVideoModalOpen from "~/atoms/recordVideoModalOpen";
|
import recordVideoModalOpen from "~/atoms/recordVideoModalOpen";
|
||||||
|
import { usePostHog } from "posthog-js/react";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
closeModal: () => void;
|
closeModal: () => void;
|
||||||
|
|
@ -45,6 +46,7 @@ export default function Recorder({ closeModal, step, setStep }: Props) {
|
||||||
const [duration, setDuration] = useState<number>(0);
|
const [duration, setDuration] = useState<number>(0);
|
||||||
const [, setPaywallOpen] = useAtom(paywallAtom);
|
const [, setPaywallOpen] = useAtom(paywallAtom);
|
||||||
const videoRef = useRef<null | HTMLVideoElement>(null);
|
const videoRef = useRef<null | HTMLVideoElement>(null);
|
||||||
|
const posthog = usePostHog();
|
||||||
|
|
||||||
const handleRecording = async () => {
|
const handleRecording = async () => {
|
||||||
const screenStream = await navigator.mediaDevices.getDisplayMedia({
|
const screenStream = await navigator.mediaDevices.getDisplayMedia({
|
||||||
|
|
@ -80,6 +82,8 @@ export default function Recorder({ closeModal, step, setStep }: Props) {
|
||||||
recorderRef.current.startRecording();
|
recorderRef.current.startRecording();
|
||||||
|
|
||||||
setStep("in");
|
setStep("in");
|
||||||
|
|
||||||
|
posthog?.capture("recorder: start video recording");
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleStop = () => {
|
const handleStop = () => {
|
||||||
|
|
@ -98,6 +102,8 @@ export default function Recorder({ closeModal, step, setStep }: Props) {
|
||||||
});
|
});
|
||||||
|
|
||||||
setStep("post");
|
setStep("post");
|
||||||
|
|
||||||
|
posthog?.capture("recorder: video recording finished");
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDelete = () => {
|
const handleDelete = () => {
|
||||||
|
|
@ -109,6 +115,8 @@ export default function Recorder({ closeModal, step, setStep }: Props) {
|
||||||
|
|
||||||
closeModal();
|
closeModal();
|
||||||
setStep("pre");
|
setStep("pre");
|
||||||
|
|
||||||
|
posthog?.capture("recorder: video deleted");
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePause = () => {
|
const handlePause = () => {
|
||||||
|
|
@ -119,6 +127,7 @@ export default function Recorder({ closeModal, step, setStep }: Props) {
|
||||||
} else {
|
} else {
|
||||||
recorderRef.current.pauseRecording();
|
recorderRef.current.pauseRecording();
|
||||||
}
|
}
|
||||||
|
posthog?.capture("recorder: recording paused/resumed", { pause });
|
||||||
setPause(!pause);
|
setPause(!pause);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -146,6 +155,8 @@ export default function Recorder({ closeModal, step, setStep }: Props) {
|
||||||
"Recording - " + dayjs().format("D MMM YYYY") + ".webm";
|
"Recording - " + dayjs().format("D MMM YYYY") + ".webm";
|
||||||
invokeSaveAsDialog(blob, dateString);
|
invokeSaveAsDialog(blob, dateString);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
posthog?.capture("recorder: video downloaded");
|
||||||
};
|
};
|
||||||
|
|
||||||
const generateThumbnail = async (video: HTMLVideoElement) => {
|
const generateThumbnail = async (video: HTMLVideoElement) => {
|
||||||
|
|
@ -187,6 +198,7 @@ export default function Recorder({ closeModal, step, setStep }: Props) {
|
||||||
.then(() => {
|
.then(() => {
|
||||||
void router.push("share/" + id);
|
void router.push("share/" + id);
|
||||||
setRecordOpen(false);
|
setRecordOpen(false);
|
||||||
|
posthog?.capture("recorder: video uploaded");
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
|
|
@ -197,6 +209,7 @@ export default function Recorder({ closeModal, step, setStep }: Props) {
|
||||||
err.message ===
|
err.message ===
|
||||||
"Sorry, you have reached the maximum video upload limit on our free tier. Please upgrade to upload more."
|
"Sorry, you have reached the maximum video upload limit on our free tier. Please upgrade to upload more."
|
||||||
) {
|
) {
|
||||||
|
posthog?.capture("recorder: video upload paywall hit");
|
||||||
setPaywallOpen(true);
|
setPaywallOpen(true);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -325,6 +338,8 @@ export default function Recorder({ closeModal, step, setStep }: Props) {
|
||||||
<video
|
<video
|
||||||
src={URL.createObjectURL(blob)}
|
src={URL.createObjectURL(blob)}
|
||||||
controls
|
controls
|
||||||
|
onPlay={() => posthog?.capture("recorder: played preview video")}
|
||||||
|
onPause={() => posthog?.capture("recorder: paused preview video")}
|
||||||
ref={videoRef}
|
ref={videoRef}
|
||||||
className="mb-4 w-[75vw]"
|
className="mb-4 w-[75vw]"
|
||||||
/>
|
/>
|
||||||
|
|
@ -374,7 +389,10 @@ export default function Recorder({ closeModal, step, setStep }: Props) {
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="ml-auto inline-flex items-center rounded-md bg-indigo-500 px-4 py-2 text-sm font-semibold leading-6 text-white shadow transition duration-150 ease-in-out hover:bg-indigo-400 disabled:cursor-not-allowed"
|
className="ml-auto inline-flex items-center rounded-md bg-indigo-500 px-4 py-2 text-sm font-semibold leading-6 text-white shadow transition duration-150 ease-in-out hover:bg-indigo-400 disabled:cursor-not-allowed"
|
||||||
onClick={() => void closeModal()}
|
onClick={() => {
|
||||||
|
posthog?.capture("recorder: closed post-modal");
|
||||||
|
void closeModal();
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
Close
|
Close
|
||||||
</button>
|
</button>
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import { Fragment, useState } from "react";
|
||||||
import { ModernSwitch } from "~/components/ModernSwitch";
|
import { ModernSwitch } from "~/components/ModernSwitch";
|
||||||
import { api, type RouterOutputs } from "~/utils/api";
|
import { api, type RouterOutputs } from "~/utils/api";
|
||||||
import ExpireDateSelectMenu from "~/components/ExpireDateSelectMenu";
|
import ExpireDateSelectMenu from "~/components/ExpireDateSelectMenu";
|
||||||
|
import { usePostHog } from "posthog-js/react";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
video: RouterOutputs["video"]["get"];
|
video: RouterOutputs["video"]["get"];
|
||||||
|
|
@ -11,6 +12,23 @@ interface Props {
|
||||||
export function ShareModal({ video }: Props) {
|
export function ShareModal({ video }: Props) {
|
||||||
const utils = api.useContext();
|
const utils = api.useContext();
|
||||||
const [open, setOpen] = useState<boolean>(false);
|
const [open, setOpen] = useState<boolean>(false);
|
||||||
|
const posthog = usePostHog();
|
||||||
|
|
||||||
|
const openModal = () => {
|
||||||
|
setOpen(true);
|
||||||
|
posthog?.capture("open video share modal", {
|
||||||
|
videoSharing: video.sharing,
|
||||||
|
videoId: video.id,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeModal = () => {
|
||||||
|
setOpen(false);
|
||||||
|
posthog?.capture("close video share modal", {
|
||||||
|
videoSharing: video.sharing,
|
||||||
|
videoId: video.id,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const setSharingMutation = api.video.setSharing.useMutation({
|
const setSharingMutation = api.video.setSharing.useMutation({
|
||||||
onMutate: async ({ videoId, sharing }) => {
|
onMutate: async ({ videoId, sharing }) => {
|
||||||
|
|
@ -58,23 +76,24 @@ export function ShareModal({ video }: Props) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setLinkCopied(false);
|
setLinkCopied(false);
|
||||||
}, 5000);
|
}, 5000);
|
||||||
|
|
||||||
|
posthog?.capture("public video link copied", {
|
||||||
|
videoSharing: video.sharing,
|
||||||
|
videoId: video.id,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<span
|
<span
|
||||||
onClick={() => setOpen(true)}
|
onClick={openModal}
|
||||||
className="ml-4 cursor-pointer rounded border border-[#0000001a] px-2 py-2 text-sm text-[#292d34] hover:bg-[#fafbfc]"
|
className="ml-4 cursor-pointer rounded border border-[#0000001a] px-2 py-2 text-sm text-[#292d34] hover:bg-[#fafbfc]"
|
||||||
>
|
>
|
||||||
Share
|
Share
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<Transition appear show={open} as={Fragment}>
|
<Transition appear show={open} as={Fragment}>
|
||||||
<Dialog
|
<Dialog as="div" className="relative z-10" onClose={closeModal}>
|
||||||
as="div"
|
|
||||||
className="relative z-10"
|
|
||||||
onClose={() => setOpen(false)}
|
|
||||||
>
|
|
||||||
<Transition.Child
|
<Transition.Child
|
||||||
as={Fragment}
|
as={Fragment}
|
||||||
enter="ease-out duration-300"
|
enter="ease-out duration-300"
|
||||||
|
|
@ -146,19 +165,6 @@ export function ShareModal({ video }: Props) {
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/*<div className="mt-3 flex h-6 items-center justify-between">*/}
|
|
||||||
{/* <span>Share link with search engines</span>*/}
|
|
||||||
{/* <ModernSwitch*/}
|
|
||||||
{/* enabled={video.linkShareSeo}*/}
|
|
||||||
{/* toggle={() => console.log("test")}*/}
|
|
||||||
{/* />*/}
|
|
||||||
{/*</div>*/}
|
|
||||||
{/*<div className="mt-3 flex h-6 items-center justify-between">*/}
|
|
||||||
{/* <span>Embed code</span>*/}
|
|
||||||
{/* <button className="h-6 rounded border border-solid border-[#d5d9df] bg-white px-[7px] font-medium">*/}
|
|
||||||
{/* Copy code*/}
|
|
||||||
{/* </button>*/}
|
|
||||||
{/*</div>*/}
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import {
|
||||||
TrashIcon,
|
TrashIcon,
|
||||||
} from "@radix-ui/react-icons";
|
} from "@radix-ui/react-icons";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
import { usePostHog } from "posthog-js/react";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
video: RouterOutputs["video"]["get"];
|
video: RouterOutputs["video"]["get"];
|
||||||
|
|
@ -18,6 +19,7 @@ export default function VideoMoreMenu({ video }: Props) {
|
||||||
const [renameMenuOpen, setRenameMenuOpen] = useState<boolean>(false);
|
const [renameMenuOpen, setRenameMenuOpen] = useState<boolean>(false);
|
||||||
const utils = api.useContext();
|
const utils = api.useContext();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const posthog = usePostHog();
|
||||||
|
|
||||||
const items = [
|
const items = [
|
||||||
{
|
{
|
||||||
|
|
@ -42,7 +44,10 @@ export default function VideoMoreMenu({ video }: Props) {
|
||||||
a.download = video.title;
|
a.download = video.title;
|
||||||
a.click();
|
a.click();
|
||||||
});
|
});
|
||||||
//window.location.href = response.url;
|
});
|
||||||
|
|
||||||
|
posthog?.capture("download existing video", {
|
||||||
|
videoId: video.id,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,12 @@ const Recorder = dynamic(() => import("~/components/Recorder"), { ssr: false });
|
||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
import { useAtom } from "jotai";
|
import { useAtom } from "jotai";
|
||||||
import recordVideoModalOpen from "~/atoms/recordVideoModalOpen";
|
import recordVideoModalOpen from "~/atoms/recordVideoModalOpen";
|
||||||
|
import { usePostHog } from "posthog-js/react";
|
||||||
|
|
||||||
export default function VideoRecordModal() {
|
export default function VideoRecordModal() {
|
||||||
const [open, setOpen] = useAtom(recordVideoModalOpen);
|
const [open, setOpen] = useAtom(recordVideoModalOpen);
|
||||||
const [step, setStep] = useState<"pre" | "in" | "post">("pre");
|
const [step, setStep] = useState<"pre" | "in" | "post">("pre");
|
||||||
|
const posthog = usePostHog();
|
||||||
|
|
||||||
function closeModal() {
|
function closeModal() {
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
|
|
@ -15,6 +17,8 @@ export default function VideoRecordModal() {
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
if (step === "pre") closeModal();
|
if (step === "pre") closeModal();
|
||||||
|
|
||||||
|
posthog?.capture("cancel video pre-recording");
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,8 @@ import axios from "axios";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useAtom } from "jotai";
|
import { useAtom } from "jotai";
|
||||||
import uploadVideoModalOpen from "~/atoms/uploadVideoModalOpen";
|
import uploadVideoModalOpen from "~/atoms/uploadVideoModalOpen";
|
||||||
|
import { usePostHog } from "posthog-js/react";
|
||||||
|
import { useSession } from "next-auth/react";
|
||||||
|
|
||||||
export default function VideoUploadModal() {
|
export default function VideoUploadModal() {
|
||||||
const [open, setOpen] = useAtom(uploadVideoModalOpen);
|
const [open, setOpen] = useAtom(uploadVideoModalOpen);
|
||||||
|
|
@ -14,6 +16,8 @@ export default function VideoUploadModal() {
|
||||||
const getSignedUrl = api.video.getUploadUrl.useMutation();
|
const getSignedUrl = api.video.getUploadUrl.useMutation();
|
||||||
const apiUtils = api.useContext();
|
const apiUtils = api.useContext();
|
||||||
const videoRef = useRef<null | HTMLVideoElement>(null);
|
const videoRef = useRef<null | HTMLVideoElement>(null);
|
||||||
|
const posthog = usePostHog();
|
||||||
|
const { data: session } = useSession();
|
||||||
|
|
||||||
const handleFileChange = (e: ChangeEvent<HTMLInputElement>): void => {
|
const handleFileChange = (e: ChangeEvent<HTMLInputElement>): void => {
|
||||||
if (e.target.files) {
|
if (e.target.files) {
|
||||||
|
|
@ -33,6 +37,10 @@ export default function VideoUploadModal() {
|
||||||
|
|
||||||
function closeModal() {
|
function closeModal() {
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
|
|
||||||
|
posthog?.capture("cancel video upload", {
|
||||||
|
stripeSubscriptionStatus: session?.user.stripeSubscriptionStatus,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSubmit = async (): Promise<void> => {
|
const handleSubmit = async (): Promise<void> => {
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,15 @@
|
||||||
import { type AppType } from "next/app";
|
import { type AppType } from "next/app";
|
||||||
import { type Session } from "next-auth";
|
import { type Session } from "next-auth";
|
||||||
import { SessionProvider } from "next-auth/react";
|
import { SessionProvider, useSession } from "next-auth/react";
|
||||||
|
|
||||||
import { api } from "~/utils/api";
|
import { api } from "~/utils/api";
|
||||||
|
|
||||||
import "~/styles/globals.css";
|
import "~/styles/globals.css";
|
||||||
import CrispChat from "~/components/CrispChat";
|
import CrispChat from "~/components/CrispChat";
|
||||||
import posthog from "posthog-js";
|
import posthog from "posthog-js";
|
||||||
import { PostHogProvider } from "posthog-js/react";
|
import { PostHogProvider, usePostHog } from "posthog-js/react";
|
||||||
import { env } from "~/env.mjs";
|
import { env } from "~/env.mjs";
|
||||||
|
import { type ReactNode, useEffect } from "react";
|
||||||
|
|
||||||
// Check that PostHog is client-side (used to handle Next.js SSR)
|
// Check that PostHog is client-side (used to handle Next.js SSR)
|
||||||
if (typeof window !== "undefined") {
|
if (typeof window !== "undefined") {
|
||||||
|
|
@ -28,11 +29,38 @@ const MyApp: AppType<{ session: Session | null }> = ({
|
||||||
return (
|
return (
|
||||||
<SessionProvider session={session}>
|
<SessionProvider session={session}>
|
||||||
<PostHogProvider client={posthog}>
|
<PostHogProvider client={posthog}>
|
||||||
|
<PostHogIdentificationWrapper>
|
||||||
<Component {...pageProps} />
|
<Component {...pageProps} />
|
||||||
|
</PostHogIdentificationWrapper>
|
||||||
<CrispChat />
|
<CrispChat />
|
||||||
</PostHogProvider>
|
</PostHogProvider>
|
||||||
</SessionProvider>
|
</SessionProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const PostHogIdentificationWrapper = ({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: ReactNode;
|
||||||
|
}) => {
|
||||||
|
const { data: session, status } = useSession();
|
||||||
|
const posthog = usePostHog();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!posthog) return;
|
||||||
|
if (status === "authenticated") {
|
||||||
|
const { id, name, email, stripeSubscriptionStatus } = session?.user;
|
||||||
|
posthog.identify(id, {
|
||||||
|
name,
|
||||||
|
email,
|
||||||
|
stripeSubscriptionStatus,
|
||||||
|
});
|
||||||
|
} else if (status === "unauthenticated") {
|
||||||
|
posthog.reset();
|
||||||
|
}
|
||||||
|
}, [posthog, session, status]);
|
||||||
|
|
||||||
|
return <div>{children}</div>;
|
||||||
|
};
|
||||||
|
|
||||||
export default api.withTRPC(MyApp);
|
export default api.withTRPC(MyApp);
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import {
|
||||||
handleSubscriptionCreatedOrUpdated,
|
handleSubscriptionCreatedOrUpdated,
|
||||||
} from "~/server/stripe-webhook-handlers";
|
} from "~/server/stripe-webhook-handlers";
|
||||||
import { stripe } from "~/server/stripe";
|
import { stripe } from "~/server/stripe";
|
||||||
|
import { posthog } from "~/server/posthog";
|
||||||
|
|
||||||
// Stripe requires the raw body to construct the event.
|
// Stripe requires the raw body to construct the event.
|
||||||
export const config = {
|
export const config = {
|
||||||
|
|
@ -41,6 +42,7 @@ export default async function handler(
|
||||||
event,
|
event,
|
||||||
stripe,
|
stripe,
|
||||||
prisma,
|
prisma,
|
||||||
|
posthog,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case "customer.subscription.created":
|
case "customer.subscription.created":
|
||||||
|
|
@ -48,6 +50,7 @@ export default async function handler(
|
||||||
await handleSubscriptionCreatedOrUpdated({
|
await handleSubscriptionCreatedOrUpdated({
|
||||||
event,
|
event,
|
||||||
prisma,
|
prisma,
|
||||||
|
posthog,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case "customer.subscription.updated":
|
case "customer.subscription.updated":
|
||||||
|
|
@ -55,6 +58,7 @@ export default async function handler(
|
||||||
await handleSubscriptionCreatedOrUpdated({
|
await handleSubscriptionCreatedOrUpdated({
|
||||||
event,
|
event,
|
||||||
prisma,
|
prisma,
|
||||||
|
posthog,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case "invoice.payment_failed":
|
case "invoice.payment_failed":
|
||||||
|
|
@ -63,6 +67,17 @@ export default async function handler(
|
||||||
// Use this webhook to notify your user that their payment has
|
// Use this webhook to notify your user that their payment has
|
||||||
// failed and to retrieve new card details.
|
// 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
|
// Can also have Stripe send an email to the customer notifying them of the failure. See settings: https://dashboard.stripe.com/settings/billing/automatic
|
||||||
|
|
||||||
|
const subscription = event.data.object as Stripe.Subscription;
|
||||||
|
const userId = subscription.metadata.userId;
|
||||||
|
|
||||||
|
if (userId) {
|
||||||
|
posthog.capture({
|
||||||
|
distinctId: userId,
|
||||||
|
event: "stripe invoice.payment_failed",
|
||||||
|
});
|
||||||
|
void posthog.shutdownAsync();
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case "customer.subscription.deleted":
|
case "customer.subscription.deleted":
|
||||||
// handle subscription cancelled automatically based
|
// handle subscription cancelled automatically based
|
||||||
|
|
@ -70,6 +85,7 @@ export default async function handler(
|
||||||
await handleSubscriptionCanceled({
|
await handleSubscriptionCanceled({
|
||||||
event,
|
event,
|
||||||
prisma,
|
prisma,
|
||||||
|
posthog,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
|
|
||||||
|
|
@ -11,17 +11,30 @@ import { ShareModal } from "~/components/ShareModal";
|
||||||
import { useSession } from "next-auth/react";
|
import { useSession } from "next-auth/react";
|
||||||
import VideoMoreMenu from "~/components/VideoMoreMenu";
|
import VideoMoreMenu from "~/components/VideoMoreMenu";
|
||||||
import ProfileMenu from "~/components/ProfileMenu";
|
import ProfileMenu from "~/components/ProfileMenu";
|
||||||
|
import { usePostHog } from "posthog-js/react";
|
||||||
|
|
||||||
const VideoList: NextPage = () => {
|
const VideoList: NextPage = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { status, data: session } = useSession();
|
const { status, data: session } = useSession();
|
||||||
const { videoId } = router.query as { videoId: string };
|
const { videoId } = router.query as { videoId: string };
|
||||||
|
const posthog = usePostHog();
|
||||||
|
|
||||||
const { data: video, isLoading } = api.video.get.useQuery(
|
const { data: video, isLoading } = api.video.get.useQuery(
|
||||||
{ videoId },
|
{ videoId },
|
||||||
{
|
{
|
||||||
enabled: router.isReady,
|
enabled: router.isReady,
|
||||||
refetchOnWindowFocus: false,
|
refetchOnWindowFocus: false,
|
||||||
|
retry: (failureCount, error) => {
|
||||||
|
if (error?.data?.code === "FORBIDDEN") return false;
|
||||||
|
else return failureCount < 2;
|
||||||
|
},
|
||||||
|
onError: (err) => {
|
||||||
|
if (err?.data?.code === "FORBIDDEN") {
|
||||||
|
posthog?.capture("video page: FORBIDDEN");
|
||||||
|
} else if (err?.data?.code === "NOT_FOUND") {
|
||||||
|
posthog?.capture("video page: NOT_FOUND");
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -33,7 +46,13 @@ const VideoList: NextPage = () => {
|
||||||
</span>
|
</span>
|
||||||
<span className="mt-3 max-w-[80%] text-center text-sm">
|
<span className="mt-3 max-w-[80%] text-center text-sm">
|
||||||
To create your own public recordings,{" "}
|
To create your own public recordings,{" "}
|
||||||
<Link href="/sign-in" className="pointer text-[#4169e1] underline">
|
<Link
|
||||||
|
onClick={() =>
|
||||||
|
posthog?.capture("click sign-up from video error page")
|
||||||
|
}
|
||||||
|
href="/sign-in"
|
||||||
|
className="pointer text-[#4169e1] underline"
|
||||||
|
>
|
||||||
create an account
|
create an account
|
||||||
</Link>{" "}
|
</Link>{" "}
|
||||||
for free!
|
for free!
|
||||||
|
|
@ -78,6 +97,30 @@ const VideoList: NextPage = () => {
|
||||||
width="100%"
|
width="100%"
|
||||||
height="100%"
|
height="100%"
|
||||||
controls={true}
|
controls={true}
|
||||||
|
onPlay={() =>
|
||||||
|
posthog?.capture("play video", {
|
||||||
|
videoId: video.id,
|
||||||
|
videoCreatedAt: video.createdAt,
|
||||||
|
videoUpdatedAt: video.updatedAt,
|
||||||
|
videoUser: video.user.id,
|
||||||
|
videoSharing: video.sharing,
|
||||||
|
videoDeleteAfterLinkExpires:
|
||||||
|
video.delete_after_link_expires,
|
||||||
|
videoShareLinkExpiresAt: video.shareLinkExpiresAt,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
onPause={() =>
|
||||||
|
posthog?.capture("pause video", {
|
||||||
|
videoId: video.id,
|
||||||
|
videoCreatedAt: video.createdAt,
|
||||||
|
videoUpdatedAt: video.updatedAt,
|
||||||
|
videoUser: video.user.id,
|
||||||
|
videoSharing: video.sharing,
|
||||||
|
videoDeleteAfterLinkExpires:
|
||||||
|
video.delete_after_link_expires,
|
||||||
|
videoShareLinkExpiresAt: video.shareLinkExpiresAt,
|
||||||
|
})
|
||||||
|
}
|
||||||
url={video.video_url}
|
url={video.video_url}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ import uploadVideoModalOpen from "~/atoms/uploadVideoModalOpen";
|
||||||
import recordVideoModalOpen from "~/atoms/recordVideoModalOpen";
|
import recordVideoModalOpen from "~/atoms/recordVideoModalOpen";
|
||||||
import Paywall from "~/components/Paywall";
|
import Paywall from "~/components/Paywall";
|
||||||
import paywallAtom from "~/atoms/paywallAtom";
|
import paywallAtom from "~/atoms/paywallAtom";
|
||||||
|
import { usePostHog } from "posthog-js/react";
|
||||||
|
|
||||||
const VideoList: NextPage = () => {
|
const VideoList: NextPage = () => {
|
||||||
const [, setRecordOpen] = useAtom(recordVideoModalOpen);
|
const [, setRecordOpen] = useAtom(recordVideoModalOpen);
|
||||||
|
|
@ -24,6 +25,7 @@ const VideoList: NextPage = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { status, data: session } = useSession();
|
const { status, data: session } = useSession();
|
||||||
const { data: videos, isLoading } = api.video.getAll.useQuery();
|
const { data: videos, isLoading } = api.video.getAll.useQuery();
|
||||||
|
const posthog = usePostHog();
|
||||||
|
|
||||||
if (status === "unauthenticated") {
|
if (status === "unauthenticated") {
|
||||||
void router.replace("/sign-in");
|
void router.replace("/sign-in");
|
||||||
|
|
@ -31,13 +33,28 @@ const VideoList: NextPage = () => {
|
||||||
|
|
||||||
const openRecordModal = () => {
|
const openRecordModal = () => {
|
||||||
setRecordOpen(true);
|
setRecordOpen(true);
|
||||||
|
|
||||||
|
posthog?.capture("open record video modal", {
|
||||||
|
stripeSubscriptionStatus: session?.user.stripeSubscriptionStatus,
|
||||||
|
cta: "empty video list page",
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const openUploadModal = () => {
|
const openUploadModal = () => {
|
||||||
if (session?.user.stripeSubscriptionStatus === "active") {
|
if (session?.user.stripeSubscriptionStatus === "active") {
|
||||||
setUploadOpen(true);
|
setUploadOpen(true);
|
||||||
|
|
||||||
|
posthog?.capture("open upload video modal", {
|
||||||
|
stripeSubscriptionStatus: session?.user.stripeSubscriptionStatus,
|
||||||
|
cta: "empty video list page",
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
setPaywallOpen(true);
|
setPaywallOpen(true);
|
||||||
|
|
||||||
|
posthog?.capture("hit video upload paywall", {
|
||||||
|
stripeSubscriptionStatus: session?.user.stripeSubscriptionStatus,
|
||||||
|
cta: "empty video list page",
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,9 +6,8 @@ import { z } from "zod";
|
||||||
export const stripeRouter = createTRPCRouter({
|
export const stripeRouter = createTRPCRouter({
|
||||||
createCheckoutSession: protectedProcedure
|
createCheckoutSession: protectedProcedure
|
||||||
.input(z.object({ billedAnnually: z.boolean() }))
|
.input(z.object({ billedAnnually: z.boolean() }))
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(
|
||||||
const { stripe, session, prisma, req } = ctx;
|
async ({ ctx: { prisma, stripe, session, req, posthog }, input }) => {
|
||||||
|
|
||||||
const customerId = await getOrCreateStripeCustomerIdForUser({
|
const customerId = await getOrCreateStripeCustomerIdForUser({
|
||||||
prisma,
|
prisma,
|
||||||
stripe,
|
stripe,
|
||||||
|
|
@ -51,11 +50,20 @@ export const stripeRouter = createTRPCRouter({
|
||||||
throw new Error("Could not create checkout session");
|
throw new Error("Could not create checkout session");
|
||||||
}
|
}
|
||||||
|
|
||||||
return { checkoutUrl: checkoutSession.url };
|
posthog.capture({
|
||||||
}),
|
distinctId: session.user.id,
|
||||||
createBillingPortalSession: protectedProcedure.mutation(async ({ ctx }) => {
|
event: "visiting checkout page",
|
||||||
const { stripe, session, prisma, req } = ctx;
|
properties: {
|
||||||
|
billingCycle: input.billedAnnually ? "annual" : "monthly",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
void posthog.shutdownAsync();
|
||||||
|
|
||||||
|
return { checkoutUrl: checkoutSession.url };
|
||||||
|
}
|
||||||
|
),
|
||||||
|
createBillingPortalSession: protectedProcedure.mutation(
|
||||||
|
async ({ ctx: { stripe, session, prisma, req, posthog } }) => {
|
||||||
const customerId = await getOrCreateStripeCustomerIdForUser({
|
const customerId = await getOrCreateStripeCustomerIdForUser({
|
||||||
prisma,
|
prisma,
|
||||||
stripe,
|
stripe,
|
||||||
|
|
@ -81,6 +89,16 @@ export const stripeRouter = createTRPCRouter({
|
||||||
throw new Error("Could not create billing portal session");
|
throw new Error("Could not create billing portal session");
|
||||||
}
|
}
|
||||||
|
|
||||||
return { billingPortalUrl: stripeBillingPortalSession.url };
|
posthog.capture({
|
||||||
}),
|
distinctId: session.user.id,
|
||||||
|
event: "visiting billing portal page",
|
||||||
|
properties: {
|
||||||
|
stripeSubscriptionStatus: session.user.stripeSubscriptionStatus,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
void posthog.shutdownAsync();
|
||||||
|
|
||||||
|
return { billingPortalUrl: stripeBillingPortalSession.url };
|
||||||
|
}
|
||||||
|
),
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -15,17 +15,27 @@ import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
|
|
||||||
export const videoRouter = createTRPCRouter({
|
export const videoRouter = createTRPCRouter({
|
||||||
getAll: protectedProcedure.query(async ({ ctx }) => {
|
getAll: protectedProcedure.query(
|
||||||
const videos = await ctx.prisma.video.findMany({
|
async ({ ctx: { prisma, session, s3, posthog } }) => {
|
||||||
|
const videos = await prisma.video.findMany({
|
||||||
where: {
|
where: {
|
||||||
userId: ctx.session.user.id,
|
userId: session.user.id,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
posthog.capture({
|
||||||
|
distinctId: session.user.id,
|
||||||
|
event: "viewing video list",
|
||||||
|
properties: {
|
||||||
|
videoAmount: videos.length,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
void posthog.shutdownAsync();
|
||||||
|
|
||||||
const videosWithThumbnailUrl = await Promise.all(
|
const videosWithThumbnailUrl = await Promise.all(
|
||||||
videos.map(async (video) => {
|
videos.map(async (video) => {
|
||||||
const thumbnailUrl = await getSignedUrl(
|
const thumbnailUrl = await getSignedUrl(
|
||||||
ctx.s3,
|
s3,
|
||||||
new GetObjectCommand({
|
new GetObjectCommand({
|
||||||
Bucket: env.AWS_BUCKET_NAME,
|
Bucket: env.AWS_BUCKET_NAME,
|
||||||
Key: video.userId + "/" + video.id + "-thumbnail",
|
Key: video.userId + "/" + video.id + "-thumbnail",
|
||||||
|
|
@ -37,12 +47,13 @@ export const videoRouter = createTRPCRouter({
|
||||||
);
|
);
|
||||||
|
|
||||||
return videosWithThumbnailUrl;
|
return videosWithThumbnailUrl;
|
||||||
}),
|
}
|
||||||
|
),
|
||||||
get: publicProcedure
|
get: publicProcedure
|
||||||
.input(z.object({ videoId: z.string() }))
|
.input(z.object({ videoId: z.string() }))
|
||||||
.query(async ({ ctx, input }) => {
|
.query(async ({ ctx, input }) => {
|
||||||
const { s3 } = ctx;
|
const { s3, posthog, session, prisma } = ctx;
|
||||||
const video = await ctx.prisma.video.findUnique({
|
const video = await prisma.video.findUnique({
|
||||||
where: {
|
where: {
|
||||||
id: input.videoId,
|
id: input.videoId,
|
||||||
},
|
},
|
||||||
|
|
@ -54,10 +65,27 @@ export const videoRouter = createTRPCRouter({
|
||||||
throw new TRPCError({ code: "NOT_FOUND" });
|
throw new TRPCError({ code: "NOT_FOUND" });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (video.userId !== ctx?.session?.user.id && !video.sharing) {
|
if (video.userId !== session?.user.id && !video.sharing) {
|
||||||
throw new TRPCError({ code: "FORBIDDEN" });
|
throw new TRPCError({ code: "FORBIDDEN" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (session) {
|
||||||
|
posthog.capture({
|
||||||
|
distinctId: session.user.id,
|
||||||
|
event: "viewing video",
|
||||||
|
properties: {
|
||||||
|
videoId: video.id,
|
||||||
|
videoCreatedAt: video.createdAt,
|
||||||
|
videoUpdatedAt: video.updatedAt,
|
||||||
|
videoUser: video.user.id,
|
||||||
|
videoSharing: video.sharing,
|
||||||
|
videoDeleteAfterLinkExpires: video.delete_after_link_expires,
|
||||||
|
videoShareLinkExpiresAt: video.shareLinkExpiresAt,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
void posthog.shutdownAsync();
|
||||||
|
}
|
||||||
|
|
||||||
const getObjectCommand = new GetObjectCommand({
|
const getObjectCommand = new GetObjectCommand({
|
||||||
Bucket: env.AWS_BUCKET_NAME,
|
Bucket: env.AWS_BUCKET_NAME,
|
||||||
Key: video.userId + "/" + video.id,
|
Key: video.userId + "/" + video.id,
|
||||||
|
|
@ -69,20 +97,29 @@ export const videoRouter = createTRPCRouter({
|
||||||
}),
|
}),
|
||||||
getUploadUrl: protectedProcedure
|
getUploadUrl: protectedProcedure
|
||||||
.input(z.object({ key: z.string() }))
|
.input(z.object({ key: z.string() }))
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx: { prisma, session, s3, posthog }, input }) => {
|
||||||
const { key } = input;
|
const { key } = input;
|
||||||
const { s3 } = ctx;
|
|
||||||
|
|
||||||
const videos = await ctx.prisma.video.findMany({
|
const videos = await prisma.video.findMany({
|
||||||
where: {
|
where: {
|
||||||
userId: ctx.session.user.id,
|
userId: session.user.id,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (
|
if (
|
||||||
videos.length >= 10 &&
|
videos.length >= 10 &&
|
||||||
ctx.session.user.stripeSubscriptionStatus !== "active"
|
session.user.stripeSubscriptionStatus !== "active"
|
||||||
) {
|
) {
|
||||||
|
posthog.capture({
|
||||||
|
distinctId: session.user.id,
|
||||||
|
event: "hit video upload limit",
|
||||||
|
properties: {
|
||||||
|
videoAmount: videos.length,
|
||||||
|
stripeSubscriptionStatus: session.user.stripeSubscriptionStatus,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
void posthog.shutdownAsync();
|
||||||
|
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "FORBIDDEN",
|
code: "FORBIDDEN",
|
||||||
message:
|
message:
|
||||||
|
|
@ -90,9 +127,19 @@ export const videoRouter = createTRPCRouter({
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const video = await ctx.prisma.video.create({
|
posthog.capture({
|
||||||
|
distinctId: session.user.id,
|
||||||
|
event: "uploading video",
|
||||||
|
properties: {
|
||||||
|
videoAmount: videos.length,
|
||||||
|
stripeSubscriptionStatus: session.user.stripeSubscriptionStatus,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
void posthog.shutdownAsync();
|
||||||
|
|
||||||
|
const video = await prisma.video.create({
|
||||||
data: {
|
data: {
|
||||||
userId: ctx.session.user.id,
|
userId: session.user.id,
|
||||||
title: key,
|
title: key,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
@ -101,7 +148,7 @@ export const videoRouter = createTRPCRouter({
|
||||||
s3,
|
s3,
|
||||||
new PutObjectCommand({
|
new PutObjectCommand({
|
||||||
Bucket: env.AWS_BUCKET_NAME,
|
Bucket: env.AWS_BUCKET_NAME,
|
||||||
Key: ctx.session.user.id + "/" + video.id,
|
Key: session.user.id + "/" + video.id,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -122,11 +169,11 @@ export const videoRouter = createTRPCRouter({
|
||||||
}),
|
}),
|
||||||
setSharing: protectedProcedure
|
setSharing: protectedProcedure
|
||||||
.input(z.object({ videoId: z.string(), sharing: z.boolean() }))
|
.input(z.object({ videoId: z.string(), sharing: z.boolean() }))
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx: { prisma, session, posthog }, input }) => {
|
||||||
const updateVideo = await ctx.prisma.video.updateMany({
|
const updateVideo = await prisma.video.updateMany({
|
||||||
where: {
|
where: {
|
||||||
id: input.videoId,
|
id: input.videoId,
|
||||||
userId: ctx.session.user.id,
|
userId: session.user.id,
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
sharing: input.sharing,
|
sharing: input.sharing,
|
||||||
|
|
@ -137,6 +184,16 @@ export const videoRouter = createTRPCRouter({
|
||||||
throw new TRPCError({ code: "FORBIDDEN" });
|
throw new TRPCError({ code: "FORBIDDEN" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
posthog.capture({
|
||||||
|
distinctId: session.user.id,
|
||||||
|
event: "update video setSharing",
|
||||||
|
properties: {
|
||||||
|
videoId: input.videoId,
|
||||||
|
videoSharing: input.sharing,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
void posthog.shutdownAsync();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
updateVideo,
|
updateVideo,
|
||||||
|
|
@ -146,11 +203,11 @@ export const videoRouter = createTRPCRouter({
|
||||||
.input(
|
.input(
|
||||||
z.object({ videoId: z.string(), delete_after_link_expires: z.boolean() })
|
z.object({ videoId: z.string(), delete_after_link_expires: z.boolean() })
|
||||||
)
|
)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx: { prisma, session, posthog }, input }) => {
|
||||||
const updateVideo = await ctx.prisma.video.updateMany({
|
const updateVideo = await prisma.video.updateMany({
|
||||||
where: {
|
where: {
|
||||||
id: input.videoId,
|
id: input.videoId,
|
||||||
userId: ctx.session.user.id,
|
userId: session.user.id,
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
delete_after_link_expires: input.delete_after_link_expires,
|
delete_after_link_expires: input.delete_after_link_expires,
|
||||||
|
|
@ -161,6 +218,16 @@ export const videoRouter = createTRPCRouter({
|
||||||
throw new TRPCError({ code: "FORBIDDEN" });
|
throw new TRPCError({ code: "FORBIDDEN" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
posthog.capture({
|
||||||
|
distinctId: session.user.id,
|
||||||
|
event: "update video delete_after_link_expires",
|
||||||
|
properties: {
|
||||||
|
videoId: input.videoId,
|
||||||
|
delete_after_link_expires: input.delete_after_link_expires,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
void posthog.shutdownAsync();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
updateVideo,
|
updateVideo,
|
||||||
|
|
@ -173,11 +240,11 @@ export const videoRouter = createTRPCRouter({
|
||||||
shareLinkExpiresAt: z.nullable(z.date()),
|
shareLinkExpiresAt: z.nullable(z.date()),
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx: { prisma, session, posthog }, input }) => {
|
||||||
const updateVideo = await ctx.prisma.video.updateMany({
|
const updateVideo = await prisma.video.updateMany({
|
||||||
where: {
|
where: {
|
||||||
id: input.videoId,
|
id: input.videoId,
|
||||||
userId: ctx.session.user.id,
|
userId: session.user.id,
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
shareLinkExpiresAt: input.shareLinkExpiresAt,
|
shareLinkExpiresAt: input.shareLinkExpiresAt,
|
||||||
|
|
@ -188,6 +255,16 @@ export const videoRouter = createTRPCRouter({
|
||||||
throw new TRPCError({ code: "FORBIDDEN" });
|
throw new TRPCError({ code: "FORBIDDEN" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
posthog.capture({
|
||||||
|
distinctId: session.user.id,
|
||||||
|
event: "update video shareLinkExpiresAt",
|
||||||
|
properties: {
|
||||||
|
videoId: input.videoId,
|
||||||
|
shareLinkExpiresAt: input.shareLinkExpiresAt,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
void posthog.shutdownAsync();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
updateVideo,
|
updateVideo,
|
||||||
|
|
@ -200,11 +277,11 @@ export const videoRouter = createTRPCRouter({
|
||||||
title: z.string(),
|
title: z.string(),
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx: { prisma, session, posthog }, input }) => {
|
||||||
const updateVideo = await ctx.prisma.video.updateMany({
|
const updateVideo = await prisma.video.updateMany({
|
||||||
where: {
|
where: {
|
||||||
id: input.videoId,
|
id: input.videoId,
|
||||||
userId: ctx.session.user.id,
|
userId: session.user.id,
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
title: input.title,
|
title: input.title,
|
||||||
|
|
@ -215,6 +292,16 @@ export const videoRouter = createTRPCRouter({
|
||||||
throw new TRPCError({ code: "FORBIDDEN" });
|
throw new TRPCError({ code: "FORBIDDEN" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
posthog.capture({
|
||||||
|
distinctId: session.user.id,
|
||||||
|
event: "update video title",
|
||||||
|
properties: {
|
||||||
|
videoId: input.videoId,
|
||||||
|
title: input.title,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
void posthog.shutdownAsync();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
updateVideo,
|
updateVideo,
|
||||||
|
|
@ -226,11 +313,11 @@ export const videoRouter = createTRPCRouter({
|
||||||
videoId: z.string(),
|
videoId: z.string(),
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx: { prisma, session, s3, posthog }, input }) => {
|
||||||
const deleteVideo = await ctx.prisma.video.deleteMany({
|
const deleteVideo = await prisma.video.deleteMany({
|
||||||
where: {
|
where: {
|
||||||
id: input.videoId,
|
id: input.videoId,
|
||||||
userId: ctx.session.user.id,
|
userId: session.user.id,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -238,17 +325,26 @@ export const videoRouter = createTRPCRouter({
|
||||||
throw new TRPCError({ code: "FORBIDDEN" });
|
throw new TRPCError({ code: "FORBIDDEN" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleteVideoObject = await ctx.s3.send(
|
posthog.capture({
|
||||||
|
distinctId: session.user.id,
|
||||||
|
event: "video delete",
|
||||||
|
properties: {
|
||||||
|
videoId: input.videoId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
void posthog.shutdownAsync();
|
||||||
|
|
||||||
|
const deleteVideoObject = await s3.send(
|
||||||
new DeleteObjectCommand({
|
new DeleteObjectCommand({
|
||||||
Bucket: env.AWS_BUCKET_NAME,
|
Bucket: env.AWS_BUCKET_NAME,
|
||||||
Key: ctx.session.user.id + "/" + input.videoId,
|
Key: session.user.id + "/" + input.videoId,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
const deleteThumbnailObject = await ctx.s3.send(
|
const deleteThumbnailObject = await s3.send(
|
||||||
new DeleteObjectCommand({
|
new DeleteObjectCommand({
|
||||||
Bucket: env.AWS_BUCKET_NAME,
|
Bucket: env.AWS_BUCKET_NAME,
|
||||||
Key: ctx.session.user.id + "/" + input.videoId + "-thumbnail",
|
Key: session.user.id + "/" + input.videoId + "-thumbnail",
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,7 @@ const createInnerTRPCContext = (opts: CreateContextOptions) => {
|
||||||
session: opts.session,
|
session: opts.session,
|
||||||
prisma,
|
prisma,
|
||||||
s3,
|
s3,
|
||||||
|
posthog,
|
||||||
stripe,
|
stripe,
|
||||||
req: opts.req,
|
req: opts.req,
|
||||||
res: opts.res,
|
res: opts.res,
|
||||||
|
|
@ -79,6 +80,7 @@ import { ZodError } from "zod";
|
||||||
import { s3 } from "~/server/aws/s3";
|
import { s3 } from "~/server/aws/s3";
|
||||||
import { stripe } from "~/server/stripe";
|
import { stripe } from "~/server/stripe";
|
||||||
import { type NextApiRequest, type NextApiResponse } from "next";
|
import { type NextApiRequest, type NextApiResponse } from "next";
|
||||||
|
import { posthog } from "~/server/posthog";
|
||||||
|
|
||||||
const t = initTRPC.context<typeof createTRPCContext>().create({
|
const t = initTRPC.context<typeof createTRPCContext>().create({
|
||||||
transformer: superjson,
|
transformer: superjson,
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import GitHubProvider from "next-auth/providers/github";
|
||||||
import { PrismaAdapter } from "@next-auth/prisma-adapter";
|
import { PrismaAdapter } from "@next-auth/prisma-adapter";
|
||||||
import { env } from "~/env.mjs";
|
import { env } from "~/env.mjs";
|
||||||
import { prisma } from "~/server/db";
|
import { prisma } from "~/server/db";
|
||||||
|
import { PostHog } from "posthog-node";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Module augmentation for `next-auth` types. Allows us to add custom properties to the `session`
|
* Module augmentation for `next-auth` types. Allows us to add custom properties to the `session`
|
||||||
|
|
@ -67,6 +68,44 @@ export const authOptions: NextAuthOptions = {
|
||||||
* @see https://next-auth.js.org/providers/github
|
* @see https://next-auth.js.org/providers/github
|
||||||
*/
|
*/
|
||||||
],
|
],
|
||||||
|
events: {
|
||||||
|
async signIn(message) {
|
||||||
|
const client = new PostHog(env.NEXT_PUBLIC_POSTHOG_KEY, {
|
||||||
|
host: env.NEXT_PUBLIC_POSTHOG_HOST,
|
||||||
|
});
|
||||||
|
|
||||||
|
client.capture({
|
||||||
|
distinctId: message.user.id,
|
||||||
|
event: "user logged in",
|
||||||
|
properties: {
|
||||||
|
provider: message.account?.provider,
|
||||||
|
isNewUser: message.isNewUser,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await client.shutdownAsync();
|
||||||
|
},
|
||||||
|
async signOut(message) {
|
||||||
|
const session = message.session as unknown as {
|
||||||
|
id: string;
|
||||||
|
sessionToken: string;
|
||||||
|
userId: string;
|
||||||
|
expires: Date;
|
||||||
|
};
|
||||||
|
if (!session?.userId) return;
|
||||||
|
|
||||||
|
const client = new PostHog(env.NEXT_PUBLIC_POSTHOG_KEY, {
|
||||||
|
host: env.NEXT_PUBLIC_POSTHOG_HOST,
|
||||||
|
});
|
||||||
|
|
||||||
|
client.capture({
|
||||||
|
distinctId: session.userId,
|
||||||
|
event: "user logged out",
|
||||||
|
});
|
||||||
|
|
||||||
|
await client.shutdownAsync();
|
||||||
|
},
|
||||||
|
},
|
||||||
pages: {
|
pages: {
|
||||||
signIn: "/sign-in",
|
signIn: "/sign-in",
|
||||||
},
|
},
|
||||||
|
|
|
||||||
6
src/server/posthog.ts
Normal file
6
src/server/posthog.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
import { PostHog } from "posthog-node";
|
||||||
|
import { env } from "~/env.mjs";
|
||||||
|
|
||||||
|
export const posthog = new PostHog(env.NEXT_PUBLIC_POSTHOG_KEY, {
|
||||||
|
host: env.NEXT_PUBLIC_POSTHOG_HOST,
|
||||||
|
});
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import type { PrismaClient } from "@prisma/client";
|
import type { PrismaClient } from "@prisma/client";
|
||||||
import type Stripe from "stripe";
|
import type Stripe from "stripe";
|
||||||
|
import { type PostHog } from "posthog-node";
|
||||||
|
|
||||||
// retrieves a Stripe customer id for a given user if it exists or creates a new one
|
// retrieves a Stripe customer id for a given user if it exists or creates a new one
|
||||||
export const getOrCreateStripeCustomerIdForUser = async ({
|
export const getOrCreateStripeCustomerIdForUser = async ({
|
||||||
|
|
@ -54,10 +55,12 @@ export const handleInvoicePaid = async ({
|
||||||
event,
|
event,
|
||||||
stripe,
|
stripe,
|
||||||
prisma,
|
prisma,
|
||||||
|
posthog,
|
||||||
}: {
|
}: {
|
||||||
event: Stripe.Event;
|
event: Stripe.Event;
|
||||||
stripe: Stripe;
|
stripe: Stripe;
|
||||||
prisma: PrismaClient;
|
prisma: PrismaClient;
|
||||||
|
posthog: PostHog;
|
||||||
}) => {
|
}) => {
|
||||||
const invoice = event.data.object as Stripe.Invoice;
|
const invoice = event.data.object as Stripe.Invoice;
|
||||||
const subscriptionId = invoice.subscription;
|
const subscriptionId = invoice.subscription;
|
||||||
|
|
@ -76,14 +79,27 @@ export const handleInvoicePaid = async ({
|
||||||
stripeSubscriptionStatus: subscription.status,
|
stripeSubscriptionStatus: subscription.status,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (userId && subscription.status) {
|
||||||
|
posthog.capture({
|
||||||
|
distinctId: userId,
|
||||||
|
event: "stripe invoice.paid",
|
||||||
|
properties: {
|
||||||
|
stripeSubscriptionStatus: subscription.status,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
void posthog.shutdownAsync();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const handleSubscriptionCreatedOrUpdated = async ({
|
export const handleSubscriptionCreatedOrUpdated = async ({
|
||||||
event,
|
event,
|
||||||
prisma,
|
prisma,
|
||||||
|
posthog,
|
||||||
}: {
|
}: {
|
||||||
event: Stripe.Event;
|
event: Stripe.Event;
|
||||||
prisma: PrismaClient;
|
prisma: PrismaClient;
|
||||||
|
posthog: PostHog;
|
||||||
}) => {
|
}) => {
|
||||||
const subscription = event.data.object as Stripe.Subscription;
|
const subscription = event.data.object as Stripe.Subscription;
|
||||||
const userId = subscription.metadata.userId;
|
const userId = subscription.metadata.userId;
|
||||||
|
|
@ -98,14 +114,24 @@ export const handleSubscriptionCreatedOrUpdated = async ({
|
||||||
stripeSubscriptionStatus: subscription.status,
|
stripeSubscriptionStatus: subscription.status,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (userId && subscription.status) {
|
||||||
|
posthog.capture({
|
||||||
|
distinctId: userId,
|
||||||
|
event: "stripe subscription created or updated",
|
||||||
|
});
|
||||||
|
void posthog.shutdownAsync();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const handleSubscriptionCanceled = async ({
|
export const handleSubscriptionCanceled = async ({
|
||||||
event,
|
event,
|
||||||
prisma,
|
prisma,
|
||||||
|
posthog,
|
||||||
}: {
|
}: {
|
||||||
event: Stripe.Event;
|
event: Stripe.Event;
|
||||||
prisma: PrismaClient;
|
prisma: PrismaClient;
|
||||||
|
posthog: PostHog;
|
||||||
}) => {
|
}) => {
|
||||||
const subscription = event.data.object as Stripe.Subscription;
|
const subscription = event.data.object as Stripe.Subscription;
|
||||||
const userId = subscription.metadata.userId;
|
const userId = subscription.metadata.userId;
|
||||||
|
|
@ -120,4 +146,12 @@ export const handleSubscriptionCanceled = async ({
|
||||||
stripeSubscriptionStatus: null,
|
stripeSubscriptionStatus: null,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (userId && subscription.status) {
|
||||||
|
posthog.capture({
|
||||||
|
distinctId: userId,
|
||||||
|
event: "stripe subscription cancelled",
|
||||||
|
});
|
||||||
|
void posthog.shutdownAsync();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue