Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions app/features/receive/unsupported-cashu-token-page.tsx

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would call this unsupported cashu token page. The token is not invalid but just not supported by our wallet atm.

Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import {
ClosePageButton,
Page,
PageContent,
PageHeader,
PageHeaderTitle,
} from '~/components/page';

type Props = {
message: string;
};

export function UnsupportedCashuTokenPage({ message }: Props) {
return (
<Page>
<PageHeader>
<ClosePageButton to="/" transition="slideDown" applyTo="oldView" />
<PageHeaderTitle>Oops!</PageHeaderTitle>
</PageHeader>
<PageContent className="flex flex-col items-center justify-center text-center">
<p>{message}</p>
</PageContent>
</Page>
);
}
23 changes: 22 additions & 1 deletion app/lib/cashu/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,15 @@ import {
Wallet,
splitAmount,
} from '@cashu/cashu-ts';
import type { Token } from '@cashu/cashu-ts';
import type { DistributedOmit } from 'type-fest';
import type { Currency, CurrencyUnit } from '../money';
import {
ExtendedMintInfo,
type ExtendedMintQuoteBolt11Response,
type MintPurpose,
} from './protocol-extensions';
import type { CashuProtocolUnit } from './types';
import { CASHU_PROTOCOL_UNITS, type CashuProtocolUnit } from './types';

const knownTestMints = [
'https://testnut.cashu.space',
Expand Down Expand Up @@ -73,6 +74,26 @@ export const getCashuProtocolUnit = (currency: Currency) => {
return currencyToCashuProtocolUnit[currency];
};

export type CashuTokenValidation =
| { isTokenSupported: true }
| { isTokenSupported: false; message: string };

/**
* Validates that a decoded Cashu token is one Agicash supports.
*/
export const validateCashuToken = (token: Token): CashuTokenValidation => {
if (
token.unit === undefined ||
!(token.unit in cashuProtocolUnitToCurrency)
) {
return {
isTokenSupported: false,
message: `This token's unit isn't supported. Supported units: ${CASHU_PROTOCOL_UNITS.join(', ')}.`,
};
}
return { isTokenSupported: true };
};

/**
* Determines the purpose of a mint based on its info.
*/
Expand Down
21 changes: 17 additions & 4 deletions app/routes/_protected.receive.cashu_.token.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { ReceiveCashuTokenQuoteService } from '~/features/receive/receive-cashu-
import { ReceiveCashuTokenService } from '~/features/receive/receive-cashu-token-service';
import { SparkReceiveQuoteRepository } from '~/features/receive/spark-receive-quote-repository';
import { SparkReceiveQuoteService } from '~/features/receive/spark-receive-quote-service';
import { UnsupportedCashuTokenPage } from '~/features/receive/unsupported-cashu-token-page';
import {
decodeCashuToken,
getCashuCryptography,
Expand All @@ -31,6 +32,7 @@ import { getUserFromCacheOrThrow } from '~/features/user/user-hooks';
import { WriteUserRepository } from '~/features/user/user-repository';
import { UserService } from '~/features/user/user-service';
import { toast } from '~/hooks/use-toast';
import { validateCashuToken } from '~/lib/cashu';
import type { Route } from './+types/_protected.receive.cashu_.token';
import { ReceiveCashuTokenSkeleton } from './receive-cashu-token-skeleton';

Expand Down Expand Up @@ -114,6 +116,15 @@ export async function clientLoader({ request }: Route.ClientLoaderArgs) {
throw redirect('/receive');
}

const validation = validateCashuToken(token);

if (!validation.isTokenSupported) {
return {
isTokenSupported: false as const,
message: validation.message,
};
}

const location = new URL(request.url);
const selectedAccountId =
location.searchParams.get('selectedAccountId') ?? undefined;
Expand Down Expand Up @@ -151,7 +162,7 @@ export async function clientLoader({ request }: Route.ClientLoaderArgs) {
throw redirect(redirectTo);
}

return { token, selectedAccountId };
return { isTokenSupported: true as const, token, selectedAccountId };
}

clientLoader.hydrate = true as const;
Expand All @@ -163,14 +174,16 @@ export function HydrateFallback() {
export default function ProtectedReceiveCashuToken({
loaderData,
}: Route.ComponentProps) {
const { token, selectedAccountId } = loaderData;
if (!loaderData.isTokenSupported) {
return <UnsupportedCashuTokenPage message={loaderData.message} />;
}

return (
<Page>
<Suspense fallback={<ReceiveCashuTokenSkeleton />}>
<ReceiveCashuToken
token={token}
preferredReceiveAccountId={selectedAccountId}
token={loaderData.token}
preferredReceiveAccountId={loaderData.selectedAccountId}
/>
</Suspense>
</Page>
Expand Down
19 changes: 16 additions & 3 deletions app/routes/_public.receive-cashu-token.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ import { Page } from '~/components/page';
import { getGiftCardByUrl } from '~/features/gift-cards/use-discover-cards';
import { LoadingScreen } from '~/features/loading/LoadingScreen';
import { PublicReceiveCashuToken } from '~/features/receive/receive-cashu-token';
import { UnsupportedCashuTokenPage } from '~/features/receive/unsupported-cashu-token-page';
import { decodeCashuToken } from '~/features/shared/cashu';
import { getQueryClient } from '~/features/shared/query-client';
import { authQueryOptions } from '~/features/user/auth';
import { validateCashuToken } from '~/lib/cashu';
import { normalizeMintUrl } from '~/lib/cashu/utils';
import type { Route } from './+types/_public.receive-cashu-token';

Expand Down Expand Up @@ -88,7 +90,16 @@ export async function clientLoader({ request }: Route.ClientLoaderArgs) {
throw redirect('/home');
}

return { token };
const validation = validateCashuToken(token);

if (!validation.isTokenSupported) {
return {
isTokenSupported: false as const,
message: validation.message,
};
}

return { isTokenSupported: true as const, token };
}

clientLoader.hydrate = true as const;
Expand All @@ -100,11 +111,13 @@ export function HydrateFallback() {
export default function ReceiveCashuTokenPage({
loaderData,
}: Route.ComponentProps) {
const { token } = loaderData;
if (!loaderData.isTokenSupported) {
return <UnsupportedCashuTokenPage message={loaderData.message} />;
}

return (
<Page>
<PublicReceiveCashuToken token={token} />
<PublicReceiveCashuToken token={loaderData.token} />
</Page>
);
}
Loading