diff --git a/src/app/api/auth/patreon/[code]/route.ts b/src/app/api/patreon/auth/[code]/route.ts
similarity index 100%
rename from src/app/api/auth/patreon/[code]/route.ts
rename to src/app/api/patreon/auth/[code]/route.ts
diff --git a/src/app/api/patreon/membership/[token]/route.ts b/src/app/api/patreon/membership/[token]/route.ts
new file mode 100644
index 000000000..8c1a42e15
--- /dev/null
+++ b/src/app/api/patreon/membership/[token]/route.ts
@@ -0,0 +1,75 @@
+import { NextResponse } from 'next/server';
+
+interface PatreonMembershipResponse {
+ data: {
+ attributes: unknown;
+ id: `${number}`;
+ relationships: {
+ memberships: {
+ data: {
+ id: string; // uuid
+ type: 'member';
+ }[];
+ };
+ };
+ type: 'user';
+ };
+ included: (
+ | {
+ attributes: {
+ patron_status:
+ | 'active_patron'
+ | 'declined_patron'
+ | 'former_patron'
+ | null; // null = never a paid member
+ };
+ id: string; // uuid
+ relationships: {
+ campaign: {
+ data: {
+ id: `${number}`; // id of my campaign to check against
+ type: 'campaign';
+ };
+ links: {
+ related: string; // url
+ };
+ };
+ };
+ type: 'member';
+ }
+ | {
+ attributes: {};
+ id: `${number}`;
+ type: 'campaign'; // ignore this discriminator
+ }
+ )[];
+ links: {
+ self: 'https://www.patreon.com/api/oauth2/v2/user/124201389';
+ };
+}
+
+export async function GET(
+ request: Request,
+ { params }: { params: { token: string } },
+) {
+ const response = await fetch(
+ encodeURI(
+ 'https://www.patreon.com/api/oauth2/v2/identity?include=memberships.campaign&fields[member]=patron_status',
+ ),
+ { headers: { Authorization: `Bearer ${params.token}` } },
+ );
+
+ if (!response.ok) return NextResponse.json(false);
+
+ const json = (await response.json()) as PatreonMembershipResponse;
+
+ return NextResponse.json(
+ json.included.some(
+ (item) =>
+ item.type === 'member' &&
+ item.relationships.campaign.data.id ===
+ process.env.PATREON_CAMPAIGN_ID &&
+ item.attributes.patron_status === 'active_patron',
+ ),
+ );
+}
diff --git a/src/app/api/auth/patreon/refresh/[token]/route.ts b/src/app/api/patreon/refresh/[token]/route.ts
similarity index 100%
rename from src/app/api/auth/patreon/refresh/[token]/route.ts
rename to src/app/api/patreon/refresh/[token]/route.ts
diff --git a/src/app/auth/[provider]/page.tsx b/src/app/auth/[provider]/page.tsx
index 43323a52b..4c63ad7dc 100644
--- a/src/app/auth/[provider]/page.tsx
+++ b/src/app/auth/[provider]/page.tsx
@@ -51,7 +51,7 @@ export default function Page({
if (!searchParams.code) break;
const data = (await fetch(
- `/api/auth/patreon/${searchParams.code}`,
+ `/api/patreon/auth/${searchParams.code}`,
).then((response) => response.json())) as PatreonAuthResponse;
mutateApp((draft) => {
diff --git a/src/components/Ad.tsx b/src/components/Ad.tsx
index b785a6df6..5e7a973ad 100644
--- a/src/components/Ad.tsx
+++ b/src/components/Ad.tsx
@@ -1,10 +1,11 @@
'use client';
-import { Box, BoxProps, Flex, Text } from '@radix-ui/themes';
+import { Box, BoxProps, Text } from '@radix-ui/themes';
import { uniqueId } from 'lodash';
import { useEffect, useRef } from 'react';
import { Vector2Tuple } from 'three';
import { imgur } from '../core/blitzkit/imgur';
+import { useAdExempt } from '../hooks/useAdExempt';
export enum AdType {
MediumRectangleHorizontalPurple = 738182777,
@@ -28,6 +29,8 @@ export function Ad({ type, style, ...props }: AdProps) {
const id = useRef(uniqueId());
const dimensions = AD_DIMENSIONS[type];
+ useAdExempt();
+
useEffect(() => {
(window as any).msAdsQueue.push(function () {
(window as any).mmnow.render({
@@ -51,19 +54,18 @@ export function Ad({ type, style, ...props }: AdProps) {
}}
{...props}
>
-
- Advertisement
-
- {dimensions[0]}x{dimensions[1]}
-
-
+ Advertisement
+
state.logins);
@@ -191,7 +194,7 @@ export default function Navbar() {
{!logins.patreon && (