From 71541c1d5df4430923614fe5a34f25b0e9ad5803 Mon Sep 17 00:00:00 2001 From: Leif Hagen Date: Mon, 29 Jun 2026 13:18:10 -0400 Subject: [PATCH] feat: prefill API key in onboarding tester from ?token= URL param MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a user arrives at the onboarding tester with a `?token=` query parameter, decode it (URL-safe base64, no padding) and prefill the API key input. Replace the default helper text with a note explaining the key was filled from the link so the user understands where it came from and can edit if needed. The decoder fails closed: any malformed value, decoding error, or decoded result that does not look like a printable key results in the input staying empty — a bad link can never break the page. The param read runs in a useEffect so SSR and the first client render agree (both empty); the prefill happens on the second client render with no hydration mismatch. The param is named `token` rather than `key` to make it clear at a glance that the URL value is not the user's literal API key — it's the encoded form that the page decodes. --- .../activation/onboarding-tester.tsx | 50 +++++++++++++++++-- 1 file changed, 46 insertions(+), 4 deletions(-) diff --git a/src/components/activation/onboarding-tester.tsx b/src/components/activation/onboarding-tester.tsx index 960404d..023fe7a 100644 --- a/src/components/activation/onboarding-tester.tsx +++ b/src/components/activation/onboarding-tester.tsx @@ -1,10 +1,32 @@ "use client"; -import { useState } from "react"; +import { useEffect, useState } from "react"; import Link from "next/link"; import { buttonVariants } from "@/components/ui/button"; import amplitude from "@/lib/amplitude"; +// Decode a `?token=` query param back into the user's API key. The +// token is URL-safe base64 of the key with no padding. Returns null when +// the param is absent, malformed, or decodes to something that doesn't +// look like a printable key — never throws so a bad link can't break the +// page. +function decodeApiKeyFromToken(token: string | null): string | null { + if (!token) return null; + try { + const standard = token.replace(/-/g, "+").replace(/_/g, "/"); + const padding = "=".repeat((4 - (standard.length % 4)) % 4); + const decoded = atob(standard + padding); + // Gate on printable ASCII (no whitespace, no control chars, no high + // bits). `atob` will happily decode any valid base64, including + // base64 of binary data — this check confirms the result actually + // looks like a key string before we paste it into the input. + if (!decoded || !/^[\x21-\x7E]+$/.test(decoded)) return null; + return decoded; + } catch { + return null; + } +} + interface ApiResponse { success: boolean; message: string; @@ -416,6 +438,18 @@ function SectionTester({ config, apiKey }: SectionTesterProps) { export function OnboardingTester() { const [apiKey, setApiKey] = useState(""); + const [prefilledFromLink, setPrefilledFromLink] = useState(false); + + // Defer the URL-param read to useEffect so SSR and the first client + // render agree (both empty) — avoids a hydration mismatch warning. + useEffect(() => { + const params = new URLSearchParams(window.location.search); + const decoded = decodeApiKeyFromToken(params.get("token")); + if (decoded) { + setApiKey(decoded); + setPrefilledFromLink(true); + } + }, []); return (
@@ -434,9 +468,17 @@ export function OnboardingTester() { placeholder="Paste your API key here" className="w-full px-3 py-2 border rounded-md bg-fd-background text-fd-foreground focus:outline-none focus:ring-2 focus:ring-fd-ring" /> -

- Your API key is shared across all use cases below. -

+ {prefilledFromLink ? ( +

+ API key auto-filled from your link — this is your trial key. You can + edit it below if needed. It's shared across all use cases + below. +

+ ) : ( +

+ Your API key is shared across all use cases below. +

+ )}
{SECTIONS.map((config) => (