feat(stripe-atlas): mock-up dashboard to enable live testing (#50327)

This commit is contained in:
Julian Domke authored and GitHub committed 2026-09-16 14:16:46 +02:00
1 parent 4432a8beb4
commit 9cdd412bab
6 files changed
+280

No files matched your search

+1
View File
@@ -144,6 +144,7 @@ These are the layout-only TanStack files. Most hold a single product layout comp
- [x] A `routes/aws-marketplace-onboarding.tsx` ← `pages/aws-marketplace-onboarding.tsx` **Delta vs plan:** placed at root rather than under `_app/` — page uses its own `LinkAwsMarketplaceLayout` and doesn't want `AppLayout` + `DefaultLayout` wrapping.
- [x] A `routes/claim-project.tsx` ← `pages/claim-project.tsx` **Delta vs plan:** placed at root rather than under `_app/` — page uses its own `<Head>` + `<main>` layout and doesn't want `AppLayout` + `DefaultLayout` wrapping.
- [x] A `routes/join.tsx` ← `pages/join.tsx` **Delta vs plan:** placed at root rather than under `_app/` — page uses a centered-div layout and doesn't want `AppLayout` + `DefaultLayout` wrapping.
- [x] A `routes/stripe-atlas-application.tsx` ← `pages/stripe-atlas-application.tsx` (no `withAuth` — reachable logged in and logged out; uses `InterstitialLayout`, so no `AppLayout` + `DefaultLayout` wrapping)
- [x] `routes/_app/support/new.tsx` ← `pages/support/new.tsx` (sets `hideMobileMenu: true` staticData; existing page is `withAuth`-wrapped so no beforeLoad migration needed yet)
- [x] `routes/_app/support/link.tsx` ← `pages/support/link.tsx`
@@ -0,0 +1,116 @@
import { useParams } from 'common'
import { useEffect, useState } from 'react'
import { CardContent } from 'ui'
import { Admonition } from 'ui-patterns/Admonition'
import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader'
import { z } from 'zod'
import { StripeAtlasApplicationForm } from './StripeAtlasApplicationForm'
import {
InterstitialLayout,
LogoPair,
PartnerLogo,
SupabaseLogo,
} from '@/components/layouts/InterstitialLayout'
import { BASE_PATH } from '@/lib/constants'
function decodeBase64Param(base64encoded: string): Base64Data | null {
try {
const bytes = Uint8Array.fromBase64(base64encoded)
const decoded = new TextDecoder().decode(bytes)
return base64DataSchema.parse(JSON.parse(decoded))
} catch {
return null
}
}
const base64DataSchema = z.discriminatedUnion('type', [
z.object({
type: z.literal('success'),
stripeAtlasToken: z.string(),
}),
z.object({
type: z.literal('error'),
message: z.string(),
}),
])
type Base64Data = z.infer<typeof base64DataSchema>
/**
* Mockup only — the form is deliberately unwired: no prefill query, no submit mutation and no
* actions, so the page can be shared for live design feedback before the perk goes live.
*/
export const StripeAtlasApplicationScreen = () => {
const params = useParams()
// give the router a chance to parse the query params
const [hydrated, setHydrated] = useState<boolean>(false)
useEffect(() => setHydrated(true), [])
if (!hydrated) {
return (
<InterstitialLayout>
<LoadingCard />
</InterstitialLayout>
)
}
const decoded = params.data ? decodeBase64Param(params.data) : null
return (
<InterstitialLayout
logo={
<LogoPair
left={<PartnerLogo src={`${BASE_PATH}/img/icons/stripe-icon.svg`} alt="Stripe" />}
right={<SupabaseLogo />}
/>
}
title="$500 Credit Code for Stripe Atlas Merchants"
description="Confirm your details below. Once the application is confirmed, you will receive a credit code via email."
>
{decoded === null && (
<CardContent className="border-none pb-12">
<Admonition
type="caution"
title="Only Pre-Filled Applications supported"
description={
<p>
We currently only support pre-filled applications for Stripe Atlas Perks.
<br />
Please head to the Stripe Atlas dashboard and use the "Pre-fill my application"
button.
</p>
}
/>
</CardContent>
)}
{decoded?.type === 'error' && (
<CardContent className="border-none pb-12">
<Admonition
type="danger"
title="We've encountered an error"
description={<p>{decoded.message}</p>}
/>
</CardContent>
)}
{decoded?.type === 'success' && (
<CardContent className="border-none">
<StripeAtlasApplicationForm stripeAtlasToken={decoded.stripeAtlasToken} />
</CardContent>
)}
</InterstitialLayout>
)
}
const LoadingCard = () => {
return (
<CardContent className="flex flex-col gap-4 border-none">
<div className="grid grid-cols-2 gap-3">
<ShimmeringLoader className="h-10 w-full py-0" />
<ShimmeringLoader className="h-10 w-full py-0" />
</div>
<ShimmeringLoader className="h-10 w-full py-0" />
<ShimmeringLoader className="h-10 w-full py-0" />
<ShimmeringLoader className="h-10 w-full py-0" />
</CardContent>
)
}
@@ -0,0 +1,106 @@
import { zodResolver } from '@hookform/resolvers/zod'
import { useForm } from 'react-hook-form'
import { Button, Form, FormControl, FormField, Input } from 'ui'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
import { z } from 'zod'
const FormSchema = z.object({
firstname: z.string().trim().min(1, 'First name is required').max(100, 'Maximum 100 characters'),
lastname: z.string().trim().min(1, 'Last name is required').max(100, 'Maximum 100 characters'),
companyName: z
.string()
.trim()
.min(1, 'Company name is required')
.max(200, 'Maximum 200 characters'),
email: z
.string()
.trim()
.min(1, 'Email is required')
.max(254, 'Maximum 254 characters')
.email('Enter a valid email address'),
})
type FormValues = z.infer<typeof FormSchema>
type StripeAtlasApplicationFormProps = {
stripeAtlasToken: string
}
/** Mockup only — submitting runs validation and stops there, nothing is sent. */
export const StripeAtlasApplicationForm = (_props: StripeAtlasApplicationFormProps) => {
// todo(@juleswritescode): fetch data from API via props.stripeAtlasToken; currently just a mockup.
const form = useForm<FormValues>({
resolver: zodResolver(FormSchema),
defaultValues: {
firstname: 'Mockey',
lastname: 'Mockupson',
companyName: 'Acmo Ck.',
email: 'me@mo.ck',
},
})
return (
<Form {...form}>
<form noValidate className="flex flex-col gap-4" onSubmit={form.handleSubmit(() => {})}>
<div className="grid grid-cols-2 gap-3">
<FormField
control={form.control}
name="firstname"
render={({ field }) => (
<FormItemLayout layout="vertical" label="First name" id="firstname">
<FormControl>
<Input {...field} id="firstname" autoComplete="given-name" />
</FormControl>
</FormItemLayout>
)}
/>
<FormField
control={form.control}
name="lastname"
render={({ field }) => (
<FormItemLayout layout="vertical" label="Last name" id="lastname">
<FormControl>
<Input {...field} id="lastname" autoComplete="family-name" />
</FormControl>
</FormItemLayout>
)}
/>
</div>
<FormField
control={form.control}
name="companyName"
render={({ field }) => (
<FormItemLayout layout="vertical" label="Company name" id="companyName">
<FormControl>
<Input {...field} id="companyName" autoComplete="organization" />
</FormControl>
</FormItemLayout>
)}
/>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItemLayout
layout="vertical"
label="Email"
id="email"
description="The credit code will be sent to this email."
>
<FormControl>
<Input {...field} id="email" type="email" autoComplete="email" />
</FormControl>
</FormItemLayout>
)}
/>
<Button block size="medium" type="submit">
Submit Application
</Button>
</form>
</Form>
)
}
@@ -0,0 +1,25 @@
import Head from 'next/head'
import { StripeAtlasApplicationScreen } from '@/components/interfaces/StripeAtlasApplication/StripeAtlasApplication'
import { buildStudioPageTitle } from '@/lib/page-title'
import type { NextPageWithLayout } from '@/types'
const PAGE_TITLE = buildStudioPageTitle({ section: 'Stripe Atlas Application', brand: 'Supabase' })
/**
* Page needs to be pre-auth – customers will be redirected here from the
* Stripe Atlas dashboard, either directly via a static link or via a callbackURL from the mgmt API.
* The user's auth state doesn't matter for the application; what matters is the the query params of the callbackURL.
*/
const StripeAtlasApplicationPage: NextPageWithLayout = () => {
return (
<>
<Head>
<title>{PAGE_TITLE}</title>
</Head>
<StripeAtlasApplicationScreen />
</>
)
}
export default StripeAtlasApplicationPage
+21
View File
@@ -10,6 +10,7 @@
import { Route as rootRouteImport } from './routes/__root'
import { Route as VerifyEmailRouteImport } from './routes/verify-email'
import { Route as StripeAtlasApplicationRouteImport } from './routes/stripe-atlas-application'
import { Route as RedeemRouteImport } from './routes/redeem'
import { Route as MaintenanceRouteImport } from './routes/maintenance'
import { Route as LogoutRouteImport } from './routes/logout'
@@ -335,6 +336,11 @@ const VerifyEmailRoute = VerifyEmailRouteImport.update({
path: '/verify-email',
getParentRoute: () => rootRouteImport,
} as any)
const StripeAtlasApplicationRoute = StripeAtlasApplicationRouteImport.update({
id: '/stripe-atlas-application',
path: '/stripe-atlas-application',
getParentRoute: () => rootRouteImport,
} as any)
const RedeemRoute = RedeemRouteImport.update({
id: '/redeem',
path: '/redeem',
@@ -2129,6 +2135,7 @@ export interface FileRoutesByFullPath {
'/logout': typeof LogoutRoute
'/maintenance': typeof MaintenanceRoute
'/redeem': typeof RedeemRoute
'/stripe-atlas-application': typeof StripeAtlasApplicationRoute
'/verify-email': typeof VerifyEmailRoute
'/account': typeof AppAccountRouteWithChildren
'/org': typeof AppOrgRouteWithChildren
@@ -2449,6 +2456,7 @@ export interface FileRoutesByTo {
'/logout': typeof LogoutRoute
'/maintenance': typeof MaintenanceRoute
'/redeem': typeof RedeemRoute
'/stripe-atlas-application': typeof StripeAtlasApplicationRoute
'/verify-email': typeof VerifyEmailRoute
'/account': typeof AppAccountRouteWithChildren
'/organizations': typeof AppOrganizationsRoute
@@ -2758,6 +2766,7 @@ export interface FileRoutesById {
'/logout': typeof LogoutRoute
'/maintenance': typeof MaintenanceRoute
'/redeem': typeof RedeemRoute
'/stripe-atlas-application': typeof StripeAtlasApplicationRoute
'/verify-email': typeof VerifyEmailRoute
'/_app/account': typeof AppAccountRouteWithChildren
'/_app/org': typeof AppOrgRouteWithChildren
@@ -3080,6 +3089,7 @@ export interface FileRouteTypes {
| '/logout'
| '/maintenance'
| '/redeem'
| '/stripe-atlas-application'
| '/verify-email'
| '/account'
| '/org'
@@ -3400,6 +3410,7 @@ export interface FileRouteTypes {
| '/logout'
| '/maintenance'
| '/redeem'
| '/stripe-atlas-application'
| '/verify-email'
| '/account'
| '/organizations'
@@ -3708,6 +3719,7 @@ export interface FileRouteTypes {
| '/logout'
| '/maintenance'
| '/redeem'
| '/stripe-atlas-application'
| '/verify-email'
| '/_app/account'
| '/_app/org'
@@ -4031,6 +4043,7 @@ export interface RootRouteChildren {
LogoutRoute: typeof LogoutRoute
MaintenanceRoute: typeof MaintenanceRoute
RedeemRoute: typeof RedeemRoute
StripeAtlasApplicationRoute: typeof StripeAtlasApplicationRoute
VerifyEmailRoute: typeof VerifyEmailRoute
ApiCheckCnameRoute: typeof ApiCheckCnameRoute
ApiCliReleaseVersionRoute: typeof ApiCliReleaseVersionRoute
@@ -4153,6 +4166,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof VerifyEmailRouteImport
parentRoute: typeof rootRouteImport
}
'/stripe-atlas-application': {
id: '/stripe-atlas-application'
path: '/stripe-atlas-application'
fullPath: '/stripe-atlas-application'
preLoaderRoute: typeof StripeAtlasApplicationRouteImport
parentRoute: typeof rootRouteImport
}
'/redeem': {
id: '/redeem'
path: '/redeem'
@@ -7120,6 +7140,7 @@ const rootRouteChildren: RootRouteChildren = {
LogoutRoute: LogoutRoute,
MaintenanceRoute: MaintenanceRoute,
RedeemRoute: RedeemRoute,
StripeAtlasApplicationRoute: StripeAtlasApplicationRoute,
VerifyEmailRoute: VerifyEmailRoute,
ApiCheckCnameRoute: ApiCheckCnameRoute,
ApiCliReleaseVersionRoute: ApiCliReleaseVersionRoute,
@@ -0,0 +1,11 @@
import { createFileRoute } from '@tanstack/react-router'
import StripeAtlasApplicationPage from '@/pages/stripe-atlas-application'
export const Route = createFileRoute('/stripe-atlas-application')({
component: StripeAtlasApplicationRoute,
})
function StripeAtlasApplicationRoute() {
return <StripeAtlasApplicationPage dehydratedState={undefined} />
}