diff --git a/src/components/Onboarding.tsx b/src/components/Onboarding.tsx new file mode 100644 index 0000000..ac75ac5 --- /dev/null +++ b/src/components/Onboarding.tsx @@ -0,0 +1,41 @@ +import { useAuthActions } from '@convex-dev/auth/react'; +import { Button } from '~/components/ui/button'; + +export function Onboarding() { + // + const { signIn } = useAuthActions(); + + const handleSignIn = () => { + console.log('signing in'); + signIn('google', { redirectTo: location.href }); + }; + + return ( +
+
+ +
+ +
+ ); +} diff --git a/src/components/subscribe/SubscribePage.tsx b/src/components/subscribe/SubscribePage.tsx deleted file mode 100644 index 48a5e17..0000000 --- a/src/components/subscribe/SubscribePage.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import { useNavigate } from '@tanstack/react-router'; -import { track } from '@vercel/analytics/react'; -import { api } from 'convex/_generated/api'; -import { useAction } from 'convex/react'; -import { useEffect } from 'react'; -import { toast } from 'sonner'; -import { FaqSection, FounderCard, ProCard } from '~/components/subscribe'; -import { useIsPro } from '~/hooks/useIsPro'; -import { Route } from '~/routes/subscribe'; - -export function SubscribePage({ route }: { route: typeof Route }) { - // - const navigate = useNavigate(); - const startSubscription = useAction(api.subscriptions.public.startSubscription); - const { isPro } = useIsPro(); - - // redirect to balance if already subscribed - useEffect(() => { - if (isPro) { - navigate({ to: '/balance' }); - } - }, [isPro, navigate]); - - const handleSubscribe = async (product: 'pro' | 'founder') => { - track('tap_subscribe', { product }); - try { - const { paymentUrl } = await startSubscription({ product }); - location.href = paymentUrl; - } catch (error) { - console.error(error); - toast.error('Failed to start subscription.'); - } - }; - - track('subscribe', { isPro }); - - // don't render if already subscribed (will redirect) - if (isPro) return null; - - return ( -
-
-

Choose your path

- {/*

- The following offers are available during research preview, and are subject to change. -

*/} -
- -
- - -
- - - -
-

- This is a research preview. Expect issues. -

-

© 2025 isPro. All rights reserved.

-
-
- ); -} diff --git a/src/components/subscribe/faq/CreditsFaq.tsx b/src/components/subscribe/faq/CreditsFaq.tsx index 4827862..d2332e7 100644 --- a/src/components/subscribe/faq/CreditsFaq.tsx +++ b/src/components/subscribe/faq/CreditsFaq.tsx @@ -15,7 +15,7 @@ export const CreditsFaq = { advertised cost.

- Pro subscribers get $10 worth of monthly credits that never expire. + Pro subscribers get $10 worth of credits that never expire, every month.

You can also top up as much credits as you need, with zero markup from us -{' '} diff --git a/src/components/subscribe/faq/FaqSection.tsx b/src/components/subscribe/faq/FaqSection.tsx index 55c8c88..ebdc564 100644 --- a/src/components/subscribe/faq/FaqSection.tsx +++ b/src/components/subscribe/faq/FaqSection.tsx @@ -1,9 +1,10 @@ +import { type ReactNode } from 'react'; import { QuestionDialog } from '~/components/QuestionDialog'; import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '~/components/ui/accordion'; import { faqs } from './index'; -export function FaqSection() { +export function FaqSection({ questionComponent }: { questionComponent?: ReactNode }) { // return (

@@ -24,7 +25,7 @@ export function FaqSection() {

Still have questions? We're here to help!

- + {questionComponent || }
); diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts index d947b39..6ac778e 100644 --- a/src/routeTree.gen.ts +++ b/src/routeTree.gen.ts @@ -13,6 +13,7 @@ import { Route as TopUpRouteImport } from './routes/top-up' import { Route as SubscribeRouteImport } from './routes/subscribe' import { Route as SkillsRouteImport } from './routes/skills' import { Route as SchedulesRouteImport } from './routes/schedules' +import { Route as PricingRouteImport } from './routes/pricing' import { Route as BalanceRouteImport } from './routes/balance' import { Route as SplatRouteImport } from './routes/$' import { Route as PolarRouteRouteImport } from './routes/polar/route' @@ -43,6 +44,11 @@ const SchedulesRoute = SchedulesRouteImport.update({ path: '/schedules', getParentRoute: () => rootRouteImport, } as any) +const PricingRoute = PricingRouteImport.update({ + id: '/pricing', + path: '/pricing', + getParentRoute: () => rootRouteImport, +} as any) const BalanceRoute = BalanceRouteImport.update({ id: '/balance', path: '/balance', @@ -93,6 +99,7 @@ export interface FileRoutesByFullPath { '/polar': typeof PolarRouteRouteWithChildren '/$': typeof SplatRoute '/balance': typeof BalanceRoute + '/pricing': typeof PricingRoute '/schedules': typeof SchedulesRoute '/skills': typeof SkillsRoute '/subscribe': typeof SubscribeRoute @@ -108,6 +115,7 @@ export interface FileRoutesByTo { '/polar': typeof PolarRouteRouteWithChildren '/$': typeof SplatRoute '/balance': typeof BalanceRoute + '/pricing': typeof PricingRoute '/schedules': typeof SchedulesRoute '/skills': typeof SkillsRoute '/subscribe': typeof SubscribeRoute @@ -124,6 +132,7 @@ export interface FileRoutesById { '/polar': typeof PolarRouteRouteWithChildren '/$': typeof SplatRoute '/balance': typeof BalanceRoute + '/pricing': typeof PricingRoute '/schedules': typeof SchedulesRoute '/skills': typeof SkillsRoute '/subscribe': typeof SubscribeRoute @@ -141,6 +150,7 @@ export interface FileRouteTypes { | '/polar' | '/$' | '/balance' + | '/pricing' | '/schedules' | '/skills' | '/subscribe' @@ -156,6 +166,7 @@ export interface FileRouteTypes { | '/polar' | '/$' | '/balance' + | '/pricing' | '/schedules' | '/skills' | '/subscribe' @@ -171,6 +182,7 @@ export interface FileRouteTypes { | '/polar' | '/$' | '/balance' + | '/pricing' | '/schedules' | '/skills' | '/subscribe' @@ -187,6 +199,7 @@ export interface RootRouteChildren { PolarRouteRoute: typeof PolarRouteRouteWithChildren SplatRoute: typeof SplatRoute BalanceRoute: typeof BalanceRoute + PricingRoute: typeof PricingRoute SchedulesRoute: typeof SchedulesRoute SkillsRoute: typeof SkillsRoute SubscribeRoute: typeof SubscribeRoute @@ -227,6 +240,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof SchedulesRouteImport parentRoute: typeof rootRouteImport } + '/pricing': { + id: '/pricing' + path: '/pricing' + fullPath: '/pricing' + preLoaderRoute: typeof PricingRouteImport + parentRoute: typeof rootRouteImport + } '/balance': { id: '/balance' path: '/balance' @@ -311,6 +331,7 @@ const rootRouteChildren: RootRouteChildren = { PolarRouteRoute: PolarRouteRouteWithChildren, SplatRoute: SplatRoute, BalanceRoute: BalanceRoute, + PricingRoute: PricingRoute, SchedulesRoute: SchedulesRoute, SkillsRoute: SkillsRoute, SubscribeRoute: SubscribeRoute, diff --git a/src/routes/__root.tsx b/src/routes/__root.tsx index b9c6b39..37185be 100644 --- a/src/routes/__root.tsx +++ b/src/routes/__root.tsx @@ -1,8 +1,7 @@ -import { useAuthActions } from '@convex-dev/auth/react'; import { QueryClient } from '@tanstack/react-query'; import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; -import { HeadContent, Outlet, Scripts, createRootRouteWithContext } from '@tanstack/react-router'; -import { TanStackRouterDevtools } from '@tanstack/react-router-devtools'; +import { HeadContent, Outlet, Scripts, createRootRouteWithContext, useRouter } from '@tanstack/react-router'; +import { TanStackRouterDevtools } from '@tanstack/router-devtools'; import { Analytics } from '@vercel/analytics/react'; import { SpeedInsights } from '@vercel/speed-insights/react'; import { AuthLoading, Authenticated, Unauthenticated } from 'convex/react'; @@ -11,9 +10,9 @@ import { CommandMenuDialog } from '~/components/CommandMenu'; import { FeedbackDialog } from '~/components/FeedbackDialog'; import { Loading } from '~/components/Loading'; import { MainHeader } from '~/components/MainHeader'; +import { Onboarding } from '~/components/Onboarding'; import { RotatingLoadingMessage } from '~/components/RotatingLoadingMessage'; import { ScheduleIterationDialog } from '~/components/ScheduleIterationDialog'; -import { Button } from '~/components/ui/button'; import { Toaster } from '~/components/ui/sonner'; import { useCurrentUser } from '~/hooks/useCurrentUser'; import { FeedbackDialogProvider, useFeedbackDialog } from '~/hooks/useFeedbackDialog'; @@ -113,7 +112,7 @@ export default function RootLayout({ children }: { children: React.ReactNode }) - + {children}
{children}
@@ -167,34 +166,20 @@ function MainWithFeedback({ children }: { children: React.ReactNode }) { ); } -function AccessDenied() { +function AccessDenied({ children }: { children: React.ReactNode }) { // - const { signIn } = useAuthActions(); - - return ( -
- - -
- ); + const router = useRouter(); + const currentPath = router.state.location.pathname; + + if (currentPath === '/pricing') { + return ( +
+
{children}
+
+ ); + } + + return ; } // TODO: on .webmanifest: diff --git a/src/routes/pricing.tsx b/src/routes/pricing.tsx new file mode 100644 index 0000000..d8c6faa --- /dev/null +++ b/src/routes/pricing.tsx @@ -0,0 +1,54 @@ +import { useAuthActions } from '@convex-dev/auth/react'; +import { createFileRoute } from '@tanstack/react-router'; +import { track } from '@vercel/analytics/react'; +import { MessageCircle } from 'lucide-react'; +import { FaqSection, FounderCard, ProCard } from '~/components/subscribe'; +import { Button } from '~/components/ui/button'; + +export const Route = createFileRoute('/pricing')({ + component: PricingPage, +}); + +function PricingPage() { + // + const { signIn } = useAuthActions(); + + const handleGetStarted = async (product: 'pro' | 'founder') => { + track('tap_get_started_pricing', { product }); + signIn('google', { redirectTo: '/subscribe' }); + }; + + const handleAskQuestion = () => { + track('tap_ask_question_pricing'); + signIn('google', { redirectTo: '/subscribe' }); + }; + + const questionButton = ( + + ); + + return ( +
+
+

Pricing

+
+ +
+ + +
+ + + +
+

+ This is a research preview. Expect issues. +

+

© 2025 isPro. All rights reserved.

+
+
+ ); +} diff --git a/src/routes/subscribe.tsx b/src/routes/subscribe.tsx index 360c189..7a8ca21 100644 --- a/src/routes/subscribe.tsx +++ b/src/routes/subscribe.tsx @@ -1,6 +1,67 @@ -import { createFileRoute } from '@tanstack/react-router'; -import { SubscribePage } from '~/components/subscribe/SubscribePage'; +import { createFileRoute, useNavigate } from '@tanstack/react-router'; +import { track } from '@vercel/analytics/react'; +import { api } from 'convex/_generated/api'; +import { useAction } from 'convex/react'; +import { useEffect } from 'react'; +import { toast } from 'sonner'; +import { FaqSection, FounderCard, ProCard } from '~/components/subscribe'; +import { useIsPro } from '~/hooks/useIsPro'; export const Route = createFileRoute('/subscribe')({ - component: () => , + component: RouteComponent, }); + +export function RouteComponent() { + // + const navigate = useNavigate(); + const startSubscription = useAction(api.subscriptions.public.startSubscription); + const { isPro } = useIsPro(); + + // redirect to balance if already subscribed + useEffect(() => { + if (isPro) { + navigate({ to: '/balance' }); + } + }, [isPro, navigate]); + + const handleSubscribe = async (product: 'pro' | 'founder') => { + track('tap_subscribe', { product }); + try { + const { paymentUrl } = await startSubscription({ product }); + location.href = paymentUrl; + } catch (error) { + console.error(error); + toast.error('Failed to start subscription.'); + } + }; + + track('subscribe', { isPro }); + + // don't render if already subscribed (will redirect) + if (isPro) return null; + + return ( +
+
+

Choose your path

+ {/*

+ The following offers are available during research preview, and are subject to change. +

*/} +
+ +
+ + +
+ + + +
+

+ This is a research preview. Expect issues. +

+

© 2025 isPro. All rights reserved.

+
+
+ ); +}