-
+
{error ? (
@@ -45,6 +46,7 @@ function SignInPage() {
Create an account
diff --git a/src/routes/auth.sign-up.tsx b/src/routes/auth.sign-up.tsx
index 66a01b0..4ed6f15 100644
--- a/src/routes/auth.sign-up.tsx
+++ b/src/routes/auth.sign-up.tsx
@@ -6,10 +6,15 @@ import { AuthShell } from '../components/auth-shell'
import { GitHubAuthButton } from '../components/github-auth-button'
export const Route = createFileRoute('/auth/sign-up')({
+ validateSearch: (search): { next?: string } => ({
+ next: typeof search.next === 'string' ? search.next : undefined,
+ }),
component: SignUpPage,
})
function SignUpPage() {
+ const { next } = Route.useSearch()
+
return (
-
+
Sign in
diff --git a/src/routes/pricing.tsx b/src/routes/pricing.tsx
index 552cf82..ccb9a7c 100644
--- a/src/routes/pricing.tsx
+++ b/src/routes/pricing.tsx
@@ -1,13 +1,22 @@
import { Link, createFileRoute } from '@tanstack/react-router'
import { ArrowRight, Check, LoaderCircle } from 'lucide-react'
-import { useState } from 'react'
+import { useEffect, useRef, useState } from 'react'
import { OwlMark } from '../components/owl-mark'
-import { FREE_RUN_LIMIT, PAID_PLAN_IDS, PAID_PLANS } from '../lib/plans'
+import {
+ FREE_RUN_LIMIT,
+ isPaidPlan,
+ PAID_PLAN_IDS,
+ PAID_PLANS,
+} from '../lib/plans'
import type { PaidPlan, Plan } from '../lib/plans'
import { getBilling } from '../server/billing'
export const Route = createFileRoute('/pricing')({
+ // `?checkout=` resumes a subscribe click that had to sign in first.
+ validateSearch: (search): { checkout?: PaidPlan } => ({
+ checkout: isPaidPlan(search.checkout) ? search.checkout : undefined,
+ }),
loader: async () => {
const billing = await getBilling()
return {
@@ -18,14 +27,33 @@ export const Route = createFileRoute('/pricing')({
component: PricingPage,
})
+// Sign in, then come back here and go straight into `target`'s checkout.
+function signInThenCheckout(target: PaidPlan) {
+ const next = `/pricing?checkout=${target}`
+ window.location.assign(`/auth/sign-in?next=${encodeURIComponent(next)}`)
+}
+
function PricingPage() {
const { signedIn, plan } = Route.useLoaderData()
+ const { checkout } = Route.useSearch()
+ const navigate = Route.useNavigate()
const [busy, setBusy] = useState(null)
const [error, setError] = useState(null)
+ const resumed = useRef(false)
+
+ useEffect(() => {
+ if (!checkout || resumed.current) return
+ resumed.current = true
+ // Drop the param first so Back from the checkout doesn't restart it.
+ void navigate({ search: {}, replace: true })
+ if (signedIn && plan === 'free') {
+ void subscribe(checkout)
+ }
+ }, [checkout, signedIn, plan])
async function subscribe(target: PaidPlan) {
if (!signedIn) {
- window.location.assign('/auth/sign-in')
+ signInThenCheckout(target)
return
}
setBusy(target)
@@ -37,7 +65,7 @@ function PricingPage() {
body: JSON.stringify({ plan: target }),
})
if (res.status === 401) {
- window.location.assign('/auth/sign-in')
+ signInThenCheckout(target)
return
}
const data = (await res.json().catch(() => ({}))) as { url?: string }
diff --git a/src/server/github-auth.ts b/src/server/github-auth.ts
index b6301c1..3e0fcee 100644
--- a/src/server/github-auth.ts
+++ b/src/server/github-auth.ts
@@ -1,6 +1,7 @@
import { createServerFn } from '@tanstack/react-start'
import { loadDb } from '../db/load'
+import { DEFAULT_NEXT, safeNextPath } from '../lib/safe-next'
import { getEnv, getOptionalEnv } from './env'
type GitHubTokenResponse = {
@@ -23,6 +24,8 @@ const githubAuthorizeUrl = 'https://github.com/login/oauth/authorize'
const githubTokenUrl = 'https://github.com/login/oauth/access_token'
const githubUserUrl = 'https://api.github.com/user'
const sessionCookieName = 'jargons_session'
+// Where to land after the GitHub round trip, set when sign-in starts.
+const signInNextCookieName = 'jargons_sign_in_next'
export type CurrentUser = {
id: string
@@ -270,6 +273,38 @@ export async function deleteSessionCookie() {
deleteCookie(sessionCookieName, { path: '/' })
}
+// Remember where to send the user once GitHub sign-in completes. Lax is enough:
+// the cookie only has to survive the top-level redirect back from GitHub.
+export async function setSignInNext(next: unknown): Promise {
+ const path = safeNextPath(next)
+ const { setCookie, deleteCookie } =
+ await import('@tanstack/react-start/server')
+
+ if (path === DEFAULT_NEXT) {
+ deleteCookie(signInNextCookieName, { path: '/' })
+ return
+ }
+
+ setCookie(signInNextCookieName, path, {
+ maxAge: 60 * 10,
+ httpOnly: true,
+ path: '/',
+ sameSite: 'lax',
+ secure: getOptionalEnv('NODE_ENV', 'development') === 'production',
+ })
+}
+
+// Read and clear the post-sign-in destination (the dashboard when unset).
+export async function consumeSignInNext(): Promise {
+ const { getCookie, deleteCookie } =
+ await import('@tanstack/react-start/server')
+ const path = safeNextPath(getCookie(signInNextCookieName))
+
+ deleteCookie(signInNextCookieName, { path: '/' })
+
+ return path
+}
+
export async function exchangeGitHubCode(code: string) {
const response = await fetch(githubTokenUrl, {
method: 'POST',