diff --git a/@api/features/billing/webhook.ts b/@api/features/billing/webhook.ts index 711d5dd..e619c9a 100644 --- a/@api/features/billing/webhook.ts +++ b/@api/features/billing/webhook.ts @@ -37,13 +37,6 @@ export async function handleBillingWebhookRequest(ctx: ContextWithRequest) { await match(event) .with({ meta: { event_name: P.union('order_created', 'order_refunded') } }, async (e) => { - if ( - e.data.attributes.first_order_item.variant_id !== - ctx.env.LEMONSQUEEZY_LIFETIME_MEMBERSHIP_VARIANT_ID - ) { - throw new Error('Not allow variant_id') - } - const expiresAt = match(e.data.attributes.status) .with('failed', () => new Date()) .with('pending', () => new Date()) diff --git a/@api/lib/env.ts b/@api/lib/env.ts index 1317ce1..e81d519 100644 --- a/@api/lib/env.ts +++ b/@api/lib/env.ts @@ -11,7 +11,8 @@ export const envSchema = z.object({ LEMONSQUEEZY_API_KEY: z.string(), LEMONSQUEEZY_STORE_ID: z.number(), LEMONSQUEEZY_WEBHOOK_SIGNING_SECRET: z.string(), - LEMONSQUEEZY_LIFETIME_MEMBERSHIP_VARIANT_ID: z.number(), + LEMONSQUEEZY_PERSONAL_LIFETIME_ACCESS_VARIANT_ID: z.number(), + LEMONSQUEEZY_TEAM_LIFETIME_ACCESS_VARIANT_ID: z.number(), POSTHOG_HOST: z.string().url(), POSTHOG_API_KEY: z.string(), SUPPORT_EMAIL: z.string().email(), diff --git a/@api/wrangler.toml b/@api/wrangler.toml index f3e727f..1b698d4 100644 --- a/@api/wrangler.toml +++ b/@api/wrangler.toml @@ -12,7 +12,8 @@ mode = "smart" WORKER_ENV = "development" WEB_BASE_URL = "https://localhost:4000/" LEMONSQUEEZY_STORE_ID = 62784 -LEMONSQUEEZY_LIFETIME_MEMBERSHIP_VARIANT_ID = 202053 +LEMONSQUEEZY_PERSONAL_LIFETIME_ACCESS_VARIANT_ID = 202053 +LEMONSQUEEZY_TEAM_LIFETIME_ACCESS_VARIANT_ID = 256458 POSTHOG_HOST = "https://us.posthog.com" POSTHOG_API_KEY = "phc_crWHQabJDjkR3ho9RLs8PY5lzut61EhMv7ghEKoIFwH" SUPPORT_EMAIL = "dinsterizer@gmail.com" @@ -33,7 +34,8 @@ custom_domain = true WORKER_ENV = "development" WEB_BASE_URL = "https://dinstack-web-preview.dinsterizer.com/" LEMONSQUEEZY_STORE_ID = 62787 -LEMONSQUEEZY_LIFETIME_MEMBERSHIP_VARIANT_ID = 202054 +LEMONSQUEEZY_PERSONAL_LIFETIME_ACCESS_VARIANT_ID = 202054 +LEMONSQUEEZY_TEAM_LIFETIME_ACCESS_VARIANT_ID = 256465 POSTHOG_HOST = "https://us.posthog.com" POSTHOG_API_KEY = "phc_gklhvpYMslkQq2pki2H4TuG53uAfSViDll6fJN7C8Ny" SUPPORT_EMAIL = "dinsterizer@gmail.com" @@ -56,7 +58,8 @@ custom_domain = true WORKER_ENV = "production" WEB_BASE_URL = "https://dinstack-web.dinsterizer.com/" LEMONSQUEEZY_STORE_ID = 62788 -LEMONSQUEEZY_LIFETIME_MEMBERSHIP_VARIANT_ID = 202055 +LEMONSQUEEZY_PERSONAL_LIFETIME_ACCESS_VARIANT_ID = 202055 +LEMONSQUEEZY_TEAM_LIFETIME_ACCESS_VARIANT_ID = 256466 POSTHOG_HOST = "https://us.posthog.com" POSTHOG_API_KEY = "phc_jkWq40BEKkhB00QCm00j5Oc9uxmvA0YObvGdv5Hnr92" SUPPORT_EMAIL = "dinsterizer@gmail.com" diff --git a/@web/.env.local b/@web/.env.local index 9712b4d..b5e7e56 100644 --- a/@web/.env.local +++ b/@web/.env.local @@ -15,7 +15,9 @@ VITE_POSTHOG_API_KEY="phc_crWHQabJDjkR3ho9RLs8PY5lzut61EhMv7ghEKoIFwH" VITE_KNOCK_PUBLIC_API_KEY="pk_test_5ov8yrcMZht3cOqntTGNNdykRoJT0QF8tMopGyQp5-w" VITE_KNOCK_FEED_CHANNEL_ID="070321a3-7a8f-4ee5-916d-bd023d2ce252" +VITE_LEMONSQUEEZY_PERSONAL_LIFETIME_ACCESS_VARIANT_ID="202053" +VITE_LEMONSQUEEZY_TEAM_LIFETIME_ACCESS_VARIANT_ID="256458" + VITE_PUBLIC_BUCKET_BASE_URL=http://localhost:8000/public/ VITE_TURNSTILE_SITE_KEY="1x00000000000000000000AA" -VITE_LEMONSQUEEZY_LIFETIME_MEMBERSHIP_VARIANT_ID="202053" VITE_CLERK_PUBLISHABLE_KEY="pk_test_YXdha2Uta29hbGEtMC5jbGVyay5hY2NvdW50cy5kZXYk" \ No newline at end of file diff --git a/@web/.env.preview b/@web/.env.preview index 873e5d2..8ab25c2 100644 --- a/@web/.env.preview +++ b/@web/.env.preview @@ -15,7 +15,9 @@ VITE_POSTHOG_API_KEY="phc_gklhvpYMslkQq2pki2H4TuG53uAfSViDll6fJN7C8Ny" VITE_KNOCK_PUBLIC_API_KEY="pk_test_5ov8yrcMZht3cOqntTGNNdykRoJT0QF8tMopGyQp5-w" VITE_KNOCK_FEED_CHANNEL_ID="070321a3-7a8f-4ee5-916d-bd023d2ce252" +VITE_LEMONSQUEEZY_PERSONAL_LIFETIME_ACCESS_VARIANT_ID="202054" +VITE_LEMONSQUEEZY_TEAM_LIFETIME_ACCESS_VARIANT_ID="256465" + VITE_PUBLIC_BUCKET_BASE_URL=https://dinstack-public-bucket-preview.dinsterizer.com/ VITE_TURNSTILE_SITE_KEY="0x4AAAAAAAPQhKcv2lS2gz9M" -VITE_LEMONSQUEEZY_LIFETIME_MEMBERSHIP_VARIANT_ID="202054" VITE_CLERK_PUBLISHABLE_KEY="pk_test_YXdha2Uta29hbGEtMC5jbGVyay5hY2NvdW50cy5kZXYk" \ No newline at end of file diff --git a/@web/.env.production b/@web/.env.production index 62319a1..5863d32 100644 --- a/@web/.env.production +++ b/@web/.env.production @@ -15,7 +15,9 @@ VITE_POSTHOG_API_KEY="phc_jkWq40BEKkhB00QCm00j5Oc9uxmvA0YObvGdv5Hnr92" VITE_KNOCK_PUBLIC_API_KEY="pk_XTnj-5XPw1TWBxp-BlJ_JrPXHCmikazlnZk6uBqz79A" VITE_KNOCK_FEED_CHANNEL_ID="070321a3-7a8f-4ee5-916d-bd023d2ce252" +VITE_LEMONSQUEEZY_PERSONAL_LIFETIME_ACCESS_VARIANT_ID="202055" +VITE_LEMONSQUEEZY_TEAM_LIFETIME_ACCESS_VARIANT_ID="256466" + VITE_PUBLIC_BUCKET_BASE_URL=https://dinstack-public-bucket.dinsterizer.com/ VITE_TURNSTILE_SITE_KEY="0x4AAAAAAAPQhWirYwPa3Z3s" -VITE_LEMONSQUEEZY_LIFETIME_MEMBERSHIP_VARIANT_ID="202055" VITE_CLERK_PUBLISHABLE_KEY="pk_live_Y2xlcmsuZGluc3Rlcml6ZXIuY29tJA" \ No newline at end of file diff --git a/@web/components/subscription-card.tsx b/@web/components/subscription-card.tsx index fbc79d9..6d96b1e 100644 --- a/@web/components/subscription-card.tsx +++ b/@web/components/subscription-card.tsx @@ -1,34 +1,68 @@ import type { Subscription } from '@api/lib/subscription' import { Button } from '@web/components/ui/button' -import { useTenant } from '@web/lib/auth' +import { Tenant, useTenant } from '@web/lib/auth' import { env } from '@web/lib/env' import { trpc } from '@web/lib/trpc' import { CheckIcon } from 'lucide-react' +import { match } from 'ts-pattern' -const includedFeatures = [ +// TODO: fill these features +const personalFeatures = [ 'Private forum access', 'Member resources', 'Entry to annual conference', 'Official member t-shirt', ] +const teamFeatures = [...personalFeatures, 'Unlimited members'] + export function SubscriptionCard() { const tenant = useTenant() - const subscription = tenant.publicMetadata.subscriptions.find( - (s) => s.variantId === env.LEMONSQUEEZY_LIFETIME_MEMBERSHIP_VARIANT_ID && s.expiresAt === null, - ) + const subscription = match(tenant.type) + .with('user', () => { + return tenant.publicMetadata.subscriptions.find( + (s) => + s.variantId === env.LEMONSQUEEZY_PERSONAL_LIFETIME_ACCESS_VARIANT_ID && + s.expiresAt === null, + ) + }) + .with('organization', () => { + return tenant.publicMetadata.subscriptions.find( + (s) => + s.variantId === env.LEMONSQUEEZY_TEAM_LIFETIME_ACCESS_VARIANT_ID && s.expiresAt === null, + ) + }) + .exhaustive() - return
{subscription ? : }
+ return ( +
+ {subscription ? ( + + ) : ( + + )} +
+ ) } -function PaidStatus(props: { subscription: Subscription }) { +function PaidStatus(props: { subscription: Subscription; type: Tenant['type'] }) { + const features = match(props.type) + .with('user', () => personalFeatures) + .with('organization', () => teamFeatures) + .exhaustive() + + const title = match(props.type) + .with('user', () => 'Lifetime Access (Personal)') + .with('organization', () => 'Lifetime Access (Team)') + .exhaustive() + return ( -
+
-

Lifetime membership

+

{title}

- Enjoying a Lifetime of Membership! You locked in your membership on{' '} + Enjoying the lifetime Access, which was unlocked on{' '} {props.subscription.createdAt.toDateString()} @@ -50,7 +84,7 @@ function PaidStatus(props: { subscription: Subscription }) { role="list" className="mt-8 grid grid-cols-1 gap-4 text-sm leading-6 text-muted-foreground sm:grid-cols-2 sm:gap-6" > - {includedFeatures.map((feature) => ( + {features.map((feature) => (

  • - +
    + +
    diff --git a/@web/providers/auth.tsx b/@web/providers/auth.tsx index 55619a5..954d4f7 100644 --- a/@web/providers/auth.tsx +++ b/@web/providers/auth.tsx @@ -10,6 +10,7 @@ import { useNavigate } from 'react-router-dom' import { match } from 'ts-pattern' type Appearance = ComponentPropsWithoutRef['appearance'] +type RouterFn = ComponentPropsWithoutRef['routerPush'] const lightAppearance = { variables: { @@ -47,7 +48,7 @@ const darkAppearance = { } satisfies Appearance export function AuthProvider(props: { children: React.ReactNode }) { - const navigate = useNavigate() + const internalNavigate = useNavigate() const theme = useSystemStore().theme const appearance = match(theme) @@ -58,6 +59,14 @@ export function AuthProvider(props: { children: React.ReactNode }) { ) .exhaustive() + const navigate: RouterFn = (to, meta) => { + if (meta?.__internal_metadata?.navigationType === 'window') { + window.location.href = to + } else { + internalNavigate(to) + } + } + return (