Skip to content

Commit 37752e3

Browse files
committed
feat: add slack invite
1 parent 0f74163 commit 37752e3

File tree

9 files changed

+257
-5
lines changed

9 files changed

+257
-5
lines changed

apps/api/src/index.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import ipBlocker from "./middleware/ipBlock.js";
1313
import Razorpay from "razorpay";
1414
import crypto from "crypto";
1515
import { paymentService } from "./services/payment.service.js";
16+
import { verifyToken } from "./utils/auth.js";
1617

1718
dotenv.config();
1819

@@ -98,6 +99,63 @@ app.get("/test", apiLimiter, (req: Request, res: Response) => {
9899
res.status(200).json({ status: "ok", message: "Test endpoint is working" });
99100
});
100101

102+
// Slack Community Invite Endpoint (Protected)
103+
app.get("/join-community", apiLimiter, async (req: Request, res: Response) => {
104+
try {
105+
// Get token from Authorization header or query parameter
106+
let token: string | undefined;
107+
const authHeader = req.headers.authorization;
108+
109+
if (authHeader && authHeader.startsWith("Bearer ")) {
110+
token = authHeader.substring(7); // Remove "Bearer " prefix
111+
} else if (req.query.token && typeof req.query.token === "string") {
112+
token = req.query.token;
113+
}
114+
115+
if (!token) {
116+
return res.status(401).json({ error: "Unauthorized - Missing token" });
117+
}
118+
119+
// Verify token and get user
120+
let user;
121+
try {
122+
user = await verifyToken(token);
123+
} catch (error) {
124+
return res.status(401).json({ error: "Unauthorized - Invalid token" });
125+
}
126+
127+
// Check if user has an active subscription
128+
const subscription = await prismaModule.prisma.subscription.findFirst({
129+
where: {
130+
userId: user.id,
131+
status: "active",
132+
endDate: {
133+
gte: new Date(),
134+
},
135+
},
136+
});
137+
138+
if (!subscription) {
139+
return res.status(403).json({
140+
error: "Forbidden - Active subscription required to join community",
141+
});
142+
}
143+
144+
// Get Slack invite URL from environment
145+
const slackInviteUrl = process.env.SLACK_INVITE_URL;
146+
if (!slackInviteUrl) {
147+
console.error("SLACK_INVITE_URL not configured");
148+
return res.status(500).json({ error: "Community invite not configured" });
149+
}
150+
151+
// Redirect to Slack community
152+
return res.redirect(slackInviteUrl);
153+
} catch (error: any) {
154+
console.error("Community invite error:", error);
155+
return res.status(500).json({ error: "Internal server error" });
156+
}
157+
});
158+
101159
// Razorpay Webhook Handler (Backup Flow)
102160
app.post("/webhook/razorpay", async (req: Request, res: Response) => {
103161
try {

apps/api/src/routers/user.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
1-
import { router, publicProcedure } from "../trpc.js";
1+
import { router, publicProcedure, protectedProcedure } from "../trpc.js";
22
import { userService } from "../services/user.service.js";
33

44
export const userRouter = router({
55
// get the total count of users
66
count: publicProcedure.query(async ({ ctx }) => {
77
return await userService.getUserCount(ctx.db.prisma);
88
}),
9+
10+
// check if current user has an active subscription
11+
subscriptionStatus: protectedProcedure.query(async ({ ctx }: any) => {
12+
const userId = ctx.user.id;
13+
return await userService.checkSubscriptionStatus(ctx.db.prisma, userId);
14+
}),
915
});

apps/api/src/services/user.service.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,35 @@ export const userService = {
1111
total_users: userCount,
1212
};
1313
},
14+
15+
/**
16+
* Check if user has an active subscription
17+
*/
18+
async checkSubscriptionStatus(prisma: PrismaClient, userId: string) {
19+
const subscription = await prisma.subscription.findFirst({
20+
where: {
21+
userId,
22+
status: "active",
23+
endDate: {
24+
gte: new Date(),
25+
},
26+
},
27+
include: {
28+
plan: true,
29+
},
30+
});
31+
32+
return {
33+
isPaidUser: !!subscription,
34+
subscription: subscription
35+
? {
36+
id: subscription.id,
37+
planName: subscription.plan?.name,
38+
startDate: subscription.startDate,
39+
endDate: subscription.endDate,
40+
status: subscription.status,
41+
}
42+
: null,
43+
};
44+
},
1445
};

apps/api/src/utils/auth.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ if (!process.env.JWT_SECRET) {
88
}
99

1010
export const generateToken = (email: string): string => {
11-
return jwt.sign({ email }, JWT_SECRET, { expiresIn: "48h" });
11+
return jwt.sign({ email }, JWT_SECRET, { expiresIn: "7d" });
1212
};
1313

1414
export const verifyToken = async (token: string) => {

apps/web/src/app/checkout/page.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import CheckoutConfirmation from "@/components/checkout/checkout-confirmation";
1+
import CheckoutWrapper from "@/components/checkout/CheckoutWrapper";
22
import Image from "next/image";
33

44
export default function Checkout() {
@@ -12,7 +12,7 @@ export default function Checkout() {
1212
priority
1313
/>
1414
<div className=" z-10">
15-
<CheckoutConfirmation></CheckoutConfirmation>
15+
<CheckoutWrapper />
1616
</div>
1717
</div>
1818
);
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
"use client";
2+
3+
import React from "react";
4+
import { useSubscription } from "@/hooks/useSubscription";
5+
import { useRouter } from "next/navigation";
6+
import CheckoutConfirmation from "./checkout-confirmation";
7+
8+
export default function CheckoutWrapper() {
9+
const { isPaidUser, isLoading } = useSubscription();
10+
const router = useRouter();
11+
12+
// Show loading state while checking subscription
13+
if (isLoading) {
14+
return (
15+
<div className="flex flex-col h-screen w-full justify-center items-center">
16+
<div className="text-white text-xl">Loading...</div>
17+
</div>
18+
);
19+
}
20+
21+
// Redirect to pricing if not a paid user
22+
if (!isPaidUser) {
23+
router.push("/pricing");
24+
return (
25+
<div className="flex flex-col h-screen w-full justify-center items-center">
26+
<div className="text-white text-xl">Redirecting...</div>
27+
</div>
28+
);
29+
}
30+
31+
// Show checkout confirmation for paid users
32+
return <CheckoutConfirmation />;
33+
}

apps/web/src/components/checkout/checkout-confirmation.tsx

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1-
import React from "react";
1+
"use client";
2+
3+
import React, { useState } from "react";
24
import { Check } from "lucide-react";
35
import { cn } from "@/lib/utils";
6+
import { useSession } from "next-auth/react";
47

58
interface CheckoutConfirmationProps {
69
className?: string;
@@ -9,6 +12,27 @@ interface CheckoutConfirmationProps {
912
const CheckoutConfirmation: React.FC<CheckoutConfirmationProps> = ({
1013
className,
1114
}) => {
15+
const { data: session } = useSession();
16+
const [error, setError] = useState<string | null>(null);
17+
18+
const handleJoinCommunity = () => {
19+
if (!session?.user) {
20+
setError("Please sign in to join the community");
21+
return;
22+
}
23+
24+
const accessToken = (session as any)?.accessToken;
25+
26+
if (!accessToken) {
27+
setError("Authentication token not found");
28+
return;
29+
}
30+
31+
// Redirect to backend endpoint which will validate subscription and redirect to Slack
32+
const apiUrl = process.env.NEXT_PUBLIC_API_URL || "http://localhost:4000";
33+
window.location.href = `${apiUrl}/join-community?token=${encodeURIComponent(accessToken)}`;
34+
};
35+
1236
return (
1337
<div className={cn("max-w-4xl mx-auto p-8 lg:p-16", className)}>
1438
<div className="relative bg-transparent border-2 border-white/20 rounded-[2rem] p-8 lg:p-16 backdrop-blur-sm">
@@ -27,10 +51,27 @@ const CheckoutConfirmation: React.FC<CheckoutConfirmationProps> = ({
2751
with the next steps from our side.
2852
</p>
2953

54+
<p className="text-lg lg:text-xl text-white/90 leading-relaxed font-light max-w-3xl mx-auto">
55+
Click on "Join" button below to join the Opensox premium community.
56+
</p>
57+
3058
<p className="text-lg lg:text-xl text-white/90 leading-relaxed font-light max-w-3xl mx-auto">
3159
If you have any doubts, feel free to ping us here:{" "}
3260
<span className="text-[#A970FF]">hi@opensox.ai</span>
3361
</p>
62+
63+
{/* Join Community Button - Only shown when logged in */}
64+
{session?.user && (
65+
<div className="pt-4">
66+
<button
67+
onClick={handleJoinCommunity}
68+
className="px-8 py-3 bg-[#A970FF] hover:bg-[#9255E8] text-white font-semibold rounded-lg transition-colors duration-200"
69+
>
70+
Join
71+
</button>
72+
{error && <p className="text-red-400 text-sm mt-2">{error}</p>}
73+
</div>
74+
)}
3475
</div>
3576
</div>
3677
</div>
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { useEffect } from "react";
2+
import { useSession } from "next-auth/react";
3+
import { trpc } from "@/lib/trpc";
4+
import { useSubscriptionStore } from "@/store/useSubscriptionStore";
5+
6+
/**
7+
* Custom hook to fetch and manage user subscription status
8+
* This hook automatically fetches subscription status when user is logged in
9+
* and updates the global subscription store
10+
*/
11+
export function useSubscription() {
12+
const { data: session, status } = useSession();
13+
const {
14+
setSubscriptionStatus,
15+
setLoading,
16+
reset,
17+
isPaidUser,
18+
subscription,
19+
isLoading,
20+
} = useSubscriptionStore();
21+
22+
// Fetch subscription status using tRPC
23+
const { data, isLoading: isFetching } = (
24+
trpc.user as any
25+
).subscriptionStatus.useQuery(undefined, {
26+
enabled: !!session?.user && status === "authenticated",
27+
refetchOnWindowFocus: false,
28+
refetchOnMount: true,
29+
});
30+
31+
useEffect(() => {
32+
if (status === "loading" || isFetching) {
33+
setLoading(true);
34+
return;
35+
}
36+
37+
if (status === "unauthenticated") {
38+
reset();
39+
return;
40+
}
41+
42+
if (data) {
43+
setSubscriptionStatus(data.isPaidUser, data.subscription);
44+
}
45+
}, [data, status, isFetching, setSubscriptionStatus, setLoading, reset]);
46+
47+
return {
48+
isPaidUser,
49+
subscription,
50+
isLoading,
51+
};
52+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { create } from "zustand";
2+
3+
interface Subscription {
4+
id: string;
5+
planName: string | null;
6+
startDate: Date;
7+
endDate: Date;
8+
status: string;
9+
}
10+
11+
interface SubscriptionState {
12+
isPaidUser: boolean;
13+
subscription: Subscription | null;
14+
isLoading: boolean;
15+
setSubscriptionStatus: (
16+
isPaidUser: boolean,
17+
subscription: Subscription | null
18+
) => void;
19+
setLoading: (isLoading: boolean) => void;
20+
reset: () => void;
21+
}
22+
23+
export const useSubscriptionStore = create<SubscriptionState>((set) => ({
24+
isPaidUser: false,
25+
subscription: null,
26+
isLoading: true,
27+
setSubscriptionStatus: (isPaidUser, subscription) =>
28+
set({ isPaidUser, subscription, isLoading: false }),
29+
setLoading: (isLoading) => set({ isLoading }),
30+
reset: () => set({ isPaidUser: false, subscription: null, isLoading: true }),
31+
}));

0 commit comments

Comments
 (0)