diff --git a/.env.example b/.env.example
index 63a9f65..a1a7bed 100644
--- a/.env.example
+++ b/.env.example
@@ -5,8 +5,7 @@ NEXT_PUBLIC_PAYPAL_CLIENT_ID = "replace me"
NEXT_PUBLIC_STRIPE_TCL_LINK = "replace me"
NEXT_PUBLIC_STRIPE_PL_LINK = "replace me"
-SLACK_TOKEN = "replace me"
-SLACK_PAYMENTS_CHANNEL = "replace me"
+SLACK_PAYMENTS_WEBHOOK_URL = "replace me"
# Spotify Now Playing
# https://developer.spotify.com/documentation/web-api/reference/get-the-users-currently-playing-track
diff --git a/app/api/paypal/orders/[orderId]/capture/route.ts b/app/api/paypal/orders/[orderId]/capture/route.ts
new file mode 100644
index 0000000..f46cb84
--- /dev/null
+++ b/app/api/paypal/orders/[orderId]/capture/route.ts
@@ -0,0 +1,40 @@
+import { NextRequest } from 'next/server'
+import { z } from 'zod'
+
+import { APIResponse } from '@/lib/api'
+import { capturePayPalPayment } from '@/lib/paypal'
+import { notifyOfPayPalPurchase } from '@/lib/slack'
+
+const CapturePaymentSchema = z.object({
+ orderId: z.string(),
+})
+
+export const POST = async (req: NextRequest, { params }: { params: { orderId: string } }) => {
+ try {
+ const safeParams = CapturePaymentSchema.parse(params)
+
+ const order = await capturePayPalPayment(safeParams.orderId)
+
+ console.log('Captured PayPal Payment', order)
+
+ // send email to customer
+
+ // send slack message to admin
+ try {
+ await notifyOfPayPalPurchase(
+ order.payer.email_address,
+ order.purchase_units[0].payments.captures[0].seller_receivable_breakdown.net_amount.value,
+ )
+ } catch (e) {}
+
+ return APIResponse.success(order, 201)
+ } catch (error: any) {
+ console.error('[API] Error capturing PayPal payment', error)
+
+ if (error instanceof z.ZodError) {
+ return APIResponse.error('Invalid data', 422, error.errors)
+ }
+
+ return APIResponse.error(error.message)
+ }
+}
diff --git a/app/api/paypal/orders/route.ts b/app/api/paypal/orders/route.ts
new file mode 100644
index 0000000..86f8452
--- /dev/null
+++ b/app/api/paypal/orders/route.ts
@@ -0,0 +1,31 @@
+import { NextRequest } from 'next/server'
+import { z } from 'zod'
+
+import { ProductPrices, ProductSKU } from '@/config/products'
+import { APIResponse } from '@/lib/api'
+import { createPayPalOrder } from '@/lib/paypal'
+
+const CreateOrderSchema = z.object({
+ sku: z.nativeEnum(ProductSKU),
+})
+
+export const POST = async (req: NextRequest) => {
+ try {
+ const json = await req.json()
+ const body = CreateOrderSchema.parse(json)
+
+ const order = await createPayPalOrder(ProductPrices[body.sku])
+
+ console.log('Created PayPal order', order)
+
+ return APIResponse.success(order, 201)
+ } catch (error: any) {
+ console.error('[API] Error creating PayPal order', error)
+
+ if (error instanceof z.ZodError) {
+ return APIResponse.error('Invalid data', 422, error.errors)
+ }
+
+ return APIResponse.error(error.message)
+ }
+}
diff --git a/app/api/spotify/route.ts b/app/api/spotify/route.ts
new file mode 100644
index 0000000..537f4a3
--- /dev/null
+++ b/app/api/spotify/route.ts
@@ -0,0 +1,47 @@
+import { z } from 'zod'
+
+import { APIResponse } from '@/lib/api'
+import { getNowPlaying } from '@/lib/spotify'
+
+export const runtime = 'edge'
+export const dynamic = 'force-dynamic'
+
+export const GET = async () => {
+ try {
+ const response = await getNowPlaying()
+
+ if (response.status === 204 || response.status > 400 || response?.data?.item === null || !response.data) {
+ return APIResponse.success({ isPlaying: false })
+ }
+
+ const song = response.data
+
+ if (song.is_playing === false) {
+ return APIResponse.success({ isPlaying: false })
+ }
+
+ const isPlaying = song.is_playing
+ const name = song.item.name
+ const artist = song.item.artists.map((_artist) => _artist.name).join(', ')
+ const album = song.item.album.name
+ const albumImage = song.item.album.images[0].url
+ const songUrl = song.item.external_urls.spotify
+
+ return APIResponse.success({
+ isPlaying,
+ name,
+ artist,
+ album,
+ albumImage,
+ songUrl,
+ })
+ } catch (error: any) {
+ console.error('[API] Error creating PayPal order', error)
+
+ if (error instanceof z.ZodError) {
+ return APIResponse.error('Invalid data', 422, error.errors)
+ }
+
+ return APIResponse.error(error.message)
+ }
+}
diff --git a/app/layout.tsx b/app/layout.tsx
index 965a246..928d223 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -8,6 +8,7 @@ import { Toaster } from 'sonner'
import { site, siteBaseMetadata } from '@/config/site'
import { cn } from '@/lib/utils'
+import { GTM } from '@/components/gtm'
import { ProgressProvider } from '@/components/progress-provider'
import { TailwindIndicator } from '@/components/tailwind-indicator'
import { ThemeProvider } from '@/components/theme-provider'
@@ -74,6 +75,7 @@ export default function RootLayout({
+