mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 17:35:10 +03:00
feat(stripe-atlas): mock-up dashboard to enable live testing (#50327)
This commit is contained in:
1 parent
4432a8beb4
commit
9cdd412bab
6 files changed
+280
No files matched your search
@@ -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>
|
||||
)
|
||||
}
|
||||
+106
@@ -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
|
||||
@@ -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} />
|
||||
}
|
||||
Reference in new issue
Block a user