diff --git a/pages/_app.tsx b/pages/_app.tsx index 9d0b965a..485540ce 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -19,7 +19,8 @@ const AUTHORIZED_UNLOGGED_URLS = [ '/signin', '/signup', '/reset-password', - '/auth/reset-password' + '/auth/reset-password', + '/app' ] function App ({ Component, pageProps }: AppProps): React.ReactElement | null { diff --git a/pages/app/index.tsx b/pages/app/index.tsx new file mode 100644 index 00000000..9f661685 --- /dev/null +++ b/pages/app/index.tsx @@ -0,0 +1,96 @@ +import React, { useEffect } from 'react' +import { GetServerSideProps } from 'next' +import { parseAddress } from 'utils/validators' + +const convertToBip21 = (queryParams: Record): string | null => { + // Extract and validate address parameter (required) + let parsedAddress: string + try { + const address = queryParams.address + if (typeof address !== 'string') { + return null + } + parsedAddress = parseAddress(address) + } catch { + return null + } + + // Build query string from all parameters except 'address' and 'b' + const queryParts: string[] = [] + for (const [key, value] of Object.entries(queryParams)) { + // Skip 'address' and 'b' parameters + if (key === 'address' || key === 'b') { + continue + } + + // Handle array values (take first element) or string values + const paramValue = Array.isArray(value) ? value[0] : value + if (paramValue !== undefined && paramValue !== '') { + queryParts.push(`${key}=${paramValue}`) + } + } + + // Construct BIP21 string + if (queryParts.length > 0) { + return `${parsedAddress}?${queryParts.join('&')}` + } + return parsedAddress +} + +export const getServerSideProps: GetServerSideProps = async (context) => { + const queryParams = context.query + + // Convert to BIP21 string (validates address internally) + const bip21String = convertToBip21(queryParams) + if (bip21String === null) { + context.res.statusCode = 400 + return { + props: { + error: 'Invalid PayButton URL.' + } + } + } + + return { + props: { + bip21String + } + } +} + +interface AppProps { + bip21String?: string + error?: string +} + +export default function App ({ bip21String, error }: AppProps): JSX.Element { + useEffect(() => { + if (error !== undefined || bip21String === undefined) { + return + } + + window.location.href = `https://cashtab.com/#/send?bip21=${bip21String}` + }, [bip21String, error]) + + if (error !== undefined) { + return ( +
+

Error

+

{error}

+
+ ) + } + + // bip21String should always be defined at that point, but this makes eslint + // happy and hardens the code a bit. + const cashtabRedirectUrl = typeof bip21String === 'string' + ? `https://cashtab.com/#/send?bip21=${bip21String}` + : 'https://cashtab.com' + + return ( +
+

Redirecting to Cashtab...

+

If you are not redirected, please click here.

+
+ ) +}