import { Form, useActionData, useLoaderData, useNavigate, useNavigation, useOutletContext } from '@remix-run/react'; import * as Drawer from '../components/Drawer'; import { BackButton } from '../components/BackButton'; import { TextInput } from '../components/TextInput'; import { useCallback, useMemo, useRef, useState } from 'react'; import { MultiSelectGroup } from '../components/MultiSelectGroup'; import { MultiSelectCard } from '../components/MultiselectCard'; import { Swap } from '../icons/Swap'; import { Book } from '../icons/Book'; import { Button } from '../components/Button'; import type { AppOutletContext } from './_dashboard.app.$appId'; import { PRODUCT_TO_ZIPPO_ROUTE_TAG, ZIPPO_ROUTE_TAG_TO_PRODUCT, validateFormData } from '../utils/utils'; import type { ActionArgs, LoaderArgs } from '@remix-run/server-runtime'; import { redirect } from '@remix-run/server-runtime'; import { json } from '@remix-run/server-runtime'; import { generateNonce, getRateLimitByTier } from '../utils/utils.server'; import { auth, sessionStorage } from '../auth.server'; import { z } from 'zod'; import type { ErrorWithGeneral } from '../types'; import { createOnChainTag, doesSessionExist, getAppById, getTeamById, updateApp, updateProvisionAccess, } from '../data/zippo.server'; import { getSignedInUser } from '../auth.server'; import { Alert } from '../components/Alert'; import * as AppSettingsConfirmDialog from '../components/AppSettingsConfirmDialog'; import { ArrowRightIcon } from '@radix-ui/react-icons'; import type { TZippoTier } from 'zippo-interface'; const updateAppModel = z.object({ nonce: z.string().min(16, 'Invalid nonce'), // 16 bytes in hex name: z.string().min(1, 'App name is required'), tagName: z.string().optional(), products: z .enum(['swap-api', 'orderbook-api']) .or(z.array(z.enum(['swap-api', 'orderbook-api'])).min(1, 'Please select at least one product')), }); type ActionInput = z.TypeOf; type Errors = ErrorWithGeneral>; export async function action({ request, params }: ActionArgs) { if (!params.appId) { throw redirect('/apps'); } const [user, headers] = await getSignedInUser(request); const session = await sessionStorage.getSession( headers.has('Set-Cookie') ? headers.get('Set-Cookie') : request.headers.get('Cookie'), ); if (!user) { throw redirect('/login', { headers }); } const nonce = session.get('nonce'); if (!nonce) { return json({ errors: { general: 'Invalid nonce' } as Errors }, { headers }); } const formData = await request.formData(); const { body, errors } = validateFormData({ formData: formData, schema: updateAppModel, }); if (errors) { return json({ errors: errors as Errors, values: body }, { headers }); } if (body.nonce !== nonce) { return json({ errors: { general: 'Invalid nonce' } as Errors, values: body }, { headers }); } // as this is an "admin" like action, we verify if the session is still valid const sessionResult = await doesSessionExist({ userId: user.id, sessionToken: user.sessionToken }); if (sessionResult.result === 'ERROR' || sessionResult.data === false) { console.warn(`User with id ${user.id} tried to update app settings but their session was invalid`); await auth.logout(request, { redirectTo: '/login' }); throw redirect('/login'); // throwing again to stop execution flow for the analyzer } // we additionally want to verify that the user has access to this app const returnedApp = await getAppById(params.appId); if (returnedApp.result === 'ERROR') { return json({ errors: { general: 'Error fetching app' } as Errors, values: body }, { headers }); } if (returnedApp.data.teamId !== user.teamId) { console.warn(`User with id ${user.id} tried to update app settings but they do not have access to this app`); return json({ errors: { general: 'You do not have access to this app' } as Errors, values: body }, { headers }); } const teamRes = await getTeamById({ teamId: user.teamId }); if (teamRes.result === 'ERROR') { return json({ errors: { general: 'Error fetching team' } as Errors, values: body }, { headers }); } const productAccess = Array.isArray(body.products) ? body.products : [body.products]; const updateResult = await updateProvisionAccess({ appId: params.appId, routeTags: productAccess.map((product) => PRODUCT_TO_ZIPPO_ROUTE_TAG[product]), rateLimits: productAccess.map(() => getRateLimitByTier((teamRes.data.tier as TZippoTier) || 'dev')), }); if (updateResult.result === 'ERROR') { return json( { errors: { general: 'Error updating app, please try again later' } as Errors, values: body }, { headers }, ); } session.set(auth.sessionKey, updateResult.data); let onChainTagId: string | undefined; if (body.tagName && !returnedApp.data.onChainTag) { const onChainTagRes = await createOnChainTag({ name: body.tagName, teamId: user.teamId, }); if (onChainTagRes.result === 'ERROR') { return json({ errors: { general: 'Error creating the onchain tag, please try again later' } as Errors, values: body, }); } onChainTagId = onChainTagRes.data.id; } if (onChainTagId || returnedApp.data.name !== body.name) { const updateResult = await updateApp({ appId: returnedApp.data.id, name: body.name, onChainTagId }); if (updateResult.result === 'ERROR') { return json( { errors: { general: 'Error persisting updated information, please try again later' } as Errors, values: body, }, { headers }, ); } } if (onChainTagId || returnedApp.data.name !== body.name) { const updateResult = await updateApp({ appId: returnedApp.data.id, name: body.name, onChainTagId }); if (updateResult.result === 'ERROR') { return json( { errors: { general: 'Error persisting updated information, please try again later' } as Errors, values: body, }, { headers }, ); } session.set(auth.sessionKey, updateResult.data); } headers.set('Set-Cookie', await sessionStorage.commitSession(session)); throw redirect(`/app/${params.appId}`, { headers }); } export async function loader({ request }: LoaderArgs) { const nonce = generateNonce(); const session = await sessionStorage.getSession(request.headers.get('Cookie')); session.flash('nonce', nonce); const header = await sessionStorage.commitSession(session); return json({ nonce }, { headers: { 'Set-Cookie': header } }); } export default function AppSettings() { const { app } = useOutletContext(); const { nonce } = useLoaderData(); const actionData = useActionData(); const ref = useRef(null); const containerRef = useRef(null); const navigate = useNavigate(); const navigation = useNavigation(); const [appName, setAppName] = useState(app.name); const [tagName, setTagName] = useState(app.onChainTag?.name ?? ''); const [showConfirmationModal, setShowConfirmationModal] = useState(false); const tagNameFieldEnabled = useMemo(() => app.onChainTag?.name === undefined, [app]); const [selectedProducts, setSelectedProducts] = useState( new Set(app.productAccess?.map((product) => ZIPPO_ROUTE_TAG_TO_PRODUCT[product]?.id)), ); const hasRemovedProducts = useMemo(() => { if (!app.productAccess) return false; if (app.productAccess.length > selectedProducts.size) { return true; } return app.productAccess.some((product) => !selectedProducts.has(ZIPPO_ROUTE_TAG_TO_PRODUCT[product]?.id)); }, [app, selectedProducts]); const addedTag = useMemo(() => { if (!tagNameFieldEnabled) return false; return tagName !== ''; }, [tagNameFieldEnabled, tagName]); const onSubmit = useCallback(() => { if (!ref.current) { console.warn('Form ref is not set'); return; } if (hasRemovedProducts) { setShowConfirmationModal(true); return; } ref.current.submit(); }, [ref, hasRemovedProducts]); return ( { if (!open) { navigate(-1); } }} >

App Settings


{actionData?.errors?.general && (
{actionData?.errors?.general}
)}
setAppName(e.target.value)} error={actionData?.errors?.name} initialValue={appName} /> { const { value, checked } = e.target as HTMLInputElement; if (checked) { selectedProducts.add(value); } else { selectedProducts.delete(value); } setSelectedProducts(new Set(selectedProducts)); }} > } id="swap-api" selected={selectedProducts.has('swap-api')} key="swap-api" className="h-full" name="products" value="swap-api" /> } id="orderbook-api" selected={selectedProducts.has('orderbook-api')} key="orderbook-api" className="h-full" name="products" value="orderbook-api" />

0x Explorer tag

To update the on-chain label for this app{' '} Contact us

setTagName(e.target.value)} error={actionData?.errors?.tagName} initialValue={tagNameFieldEnabled ? tagName : ''} />
{ if (!open) { setShowConfirmationModal(false); } }} > { ref.current?.submit(); }} />
); }