Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
7234d1b
Remove workaround added because of Vercel issue that was causing prer…
jbojcic1 May 20, 2026
0b6c32c
Merge pull request #1091 from MakePrisms/test-after-vercel-change
jbojcic1 May 21, 2026
2c13814
fix(spark): handle missing WebAssembly with friendly error UI
ditto-agent May 21, 2026
accb0db
fix(root): add mt-4 to WebAssembly error reload button
ditto-agent May 21, 2026
fb770c2
Merge pull request #1102 from MakePrisms/fix/handle-no-webassembly
jbojcic1 May 21, 2026
36b65b5
fix(cashu): sync error-codes enum to current NUT spec
orveth May 21, 2026
924e93d
Merge pull request #1105 from MakePrisms/fix/sync-cashu-error-codes-t…
gudnuf May 21, 2026
a5e8281
refactor(hooks): rename use*ReceiveQuote -> useTrack*ReceiveQuote
orveth May 21, 2026
a57cddb
Merge pull request #1099 from MakePrisms/feat/usetrack-rename
gudnuf May 21, 2026
5bd7874
fix(auth): only redirect on document navigations to avoid trapping us…
ditto-agent May 22, 2026
aaae47a
Merge pull request #1106 from MakePrisms/auth/server-redirect-session…
gudnuf May 22, 2026
8ba0cec
fix(receive): capture claim error to Sentry
orveth May 21, 2026
e68ac24
Merge pull request #1101 from MakePrisms/fix/claim-token-sentry-capture
gudnuf May 22, 2026
a266e7c
refactor: migrate from Zod to Zod Mini for smaller bundle
gudnuf May 21, 2026
8f28ed7
Merge pull request #1103 from MakePrisms/issue-733-zod-mini
gudnuf May 26, 2026
50cfd76
feat(spark): support LUD-06 description_hash on LNURL-pay
gudnuf May 26, 2026
67cc567
Merge pull request #1095 from MakePrisms/feat/lud06-spark
gudnuf May 26, 2026
89eb98e
chore: enabled parent .envrc support
pmilic021 May 27, 2026
fff4ba5
Merge pull request #1109 from MakePrisms/chore/enable-parent-envrc
pmilic021 May 27, 2026
18e2f02
fix(og): use Vercel deployment URL for absolute metadata URLs
gudnuf May 27, 2026
45cd300
Merge pull request #1110 from MakePrisms/fix/og-image-prerender-origin
gudnuf May 27, 2026
f0d0617
fix(root): resolve canonical origin in the loader so shared links are…
gudnuf May 27, 2026
3dd2123
Merge pull request #1111 from MakePrisms/fix/root-loader-canonical-or…
gudnuf May 27, 2026
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
3 changes: 3 additions & 0 deletions .envrc
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,6 @@ use devenv
# Install Node.js version from .nvmrc
eval "$(fnm env)"
fnm use --install-if-missing

# Use parent .envrc, if exists, as well. Useful for distinct configuration shared by all Agicash projects, but not by other projects on the host machine. (e.g. claude account setup)
source_up_if_exists
8 changes: 7 additions & 1 deletion app/entry.client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,13 @@ configure({
// Start Breez WASM fetch/compile as early as possible so it overlaps with
// hydration, Sentry init, and the auth query — by the time the _protected
// middleware awaits it, init is often already done.
ensureBreezWasm();
// Swallow the rejection here; the middleware re-awaits the same cached promise
// and surfaces the error through the route ErrorBoundary, where we render a
// friendly message. Without this `.catch`, the rejection also fires
// `unhandledrejection` and is reported to Sentry as a duplicate.
ensureBreezWasm().catch(() => {
// Surfaced via _protected middleware → route ErrorBoundary.
});

// Prefetch feature flags as early as possible.
// Before login the DB client uses the anon key (no access token),
Expand Down
28 changes: 6 additions & 22 deletions app/entry.server.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import type { RenderToPipeableStreamOptions } from 'react-dom/server';
import { renderToPipeableStream } from 'react-dom/server';
import type { EntryContext, RouterContextProvider } from 'react-router';
import { ServerRouter } from 'react-router';
import { PRERENDERED_PATHS } from './prerender-paths';

export const streamTimeout = 5_000;

Expand All @@ -35,17 +34,6 @@ function handleRequest(
// react-router's prerender constructs requests with no user-agent header.
const isBuildTimePrerender = !userAgent;

// Vercel currently doesn't serve prerendered HTML as static files when
// ssr:true (remix-run/react-router#14281) — requests for these paths
// fall through to the SSR function. Match them here so the outlining
// override below applies at runtime SSR too. Once the upstream adapter
// bug is fixed, this path-list check can be dropped; the no-UA check
// alone will cover real prerender invocations.
const pathname = new URL(request.url).pathname.replace(/\/+$/, '') || '/';
const isPrerenderRoute = PRERENDERED_PATHS.includes(pathname);

const shouldDisableOutlining = isBuildTimePrerender || isPrerenderRoute;

// Ensure requests from bots and SPA Mode renders wait for all content to load before responding
// https://react.dev/reference/react-dom/server/renderToPipeableStream#waiting-for-all-content-to-load-for-crawlers-and-static-generation
const readyOption: keyof RenderToPipeableStreamOptions =
Expand All @@ -67,16 +55,12 @@ function handleRequest(
//
// This trades a fallback flash for faster first paint. It's a real
// win for streaming SSR with slow data — a skeleton beats a blank
// page. But for prerender-eligible routes it's pure cost: when
// served as static (the intent), the file is buffered and there's
// no opportunity for early flushing — the fallback-then-swap dance
// still happens in the file, so the user sees a fallback flash on
// first paint with nothing gained. When falling through to runtime
// SSR (current Vercel behavior, see above), the same flash returns
// for a different reason but the same fix applies. Setting an
// effectively-infinite threshold disables outlining for these
// requests, so the content renders inline in the shell.
progressiveChunkSize: shouldDisableOutlining
// page. But for prerendered routes it's pure cost: when served as
// static (the intent), the file is buffered and there's no opportunity
// for early flushing — the fallback-then-swap dance still happens in
// the file, so the user sees a fallback flash on first paint with
// nothing gained.
progressiveChunkSize: isBuildTimePrerender
? Number.POSITIVE_INFINITY
: undefined,
[readyOption]() {
Expand Down
2 changes: 1 addition & 1 deletion app/features/accounts/account-repository.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { type QueryClient, useQueryClient } from '@tanstack/react-query';
import type { DistributedOmit } from 'type-fest';
import { z } from 'zod';
import { z } from 'zod/mini';
import { normalizeMintUrl } from '~/lib/cashu';
import { ProofSchema } from '~/lib/cashu/types';
import type { Currency } from '~/lib/money';
Expand Down
2 changes: 1 addition & 1 deletion app/features/accounts/account.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { BreezSdk } from '@agicash/breez-sdk-spark';
import type { DistributedOmit } from 'type-fest';
import { z } from 'zod';
import { z } from 'zod/mini';
import { type ExtendedCashuWallet, getCashuUnit, sumProofs } from '~/lib/cashu';
import { type Currency, Money } from '~/lib/money';
import type { SparkNetwork } from '../agicash-db/json-models/spark-account-details-db-data';
Expand Down
6 changes: 3 additions & 3 deletions app/features/accounts/cashu-account.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Proof } from '@cashu/cashu-ts';
import { z } from 'zod';
import { z } from 'zod/mini';
import { ProofSchema } from '~/lib/cashu/types';

export const CashuProofSchema = z.object({
Expand All @@ -16,8 +16,8 @@ export const CashuProofSchema = z.object({
state: z.enum(['UNSPENT', 'RESERVED', 'SPENT']),
version: z.number(),
createdAt: z.string(),
reservedAt: z.string().nullable().optional(),
spentAt: z.string().nullable().optional(),
reservedAt: z.optional(z.nullable(z.string())),
spentAt: z.optional(z.nullable(z.string())),
});

export type CashuProof = z.infer<typeof CashuProofSchema>;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { z } from 'zod';
import { z } from 'zod/mini';
import { CashuAccountDetailsDbDataSchema } from './cashu-account-details-db-data';
import { SparkAccountDetailsDbDataSchema } from './spark-account-details-db-data';

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { z } from 'zod';
import { z } from 'zod/mini';

export const CashuAccountDetailsDbDataSchema = z.object({
/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { z } from 'zod';
import { z } from 'zod/mini';
import { Money } from '~/lib/money';
import { CashuTokenMeltDbDataSchema } from './cashu-token-melt-db-data';

Expand All @@ -22,23 +22,23 @@ export const CashuLightningReceiveDbDataSchema = z.object({
/**
* The description of the transaction.
*/
description: z.string().optional(),
description: z.optional(z.string()),
/**
* Optional fee charged by the mint to deposit money into the account.
* The payer of the lightning invoice pays this fee.
*/
mintingFee: z.instanceof(Money).optional(),
mintingFee: z.optional(z.instanceof(Money)),
/**
* Amounts for each blinded message created for this receive.
* Will be set only when the receive quote gets paid.
*/
outputAmounts: z.array(z.number()).optional(),
outputAmounts: z.optional(z.array(z.number())),
/**
* The data of the cashu token melted for the receive.
* This will be set only for cashu token receives when the destination account is not the mint that issued the token (the token was melted to
* pay the lightning invoice of the mint quote from the destination mint).
*/
cashuTokenMeltData: CashuTokenMeltDbDataSchema.optional(),
cashuTokenMeltData: z.optional(CashuTokenMeltDbDataSchema),
/**
* The total fee for the transaction.
* For lightning receives this will equal to `mintingFee` or zero if the mint has no minting fee.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { z } from 'zod';
import { z } from 'zod/mini';
import { DestinationDetailsSchema } from '~/features/send/cashu-send-quote';
import { Money } from '~/lib/money';

Expand Down Expand Up @@ -52,32 +52,32 @@ export const CashuLightningSendDbDataSchema = z.object({
* Destination details of the send.
* This will be undefined if the send is directly paying a bolt11.
*/
destinationDetails: DestinationDetailsSchema.optional(),
destinationDetails: z.optional(DestinationDetailsSchema),
/**
* Preimage of the lightning payment.
* Will be set only when the send is completed.
* If the lightning payment is settled internally in the mint, this will be an empty string or '0x0000000000000000000000000000000000000000000000000000000000000000'.
*/
paymentPreimage: z.string().optional(),
paymentPreimage: z.optional(z.string()),
/**
* Amount spent on the send.
* This is the sum of `amountReceived` and `totalFee`.
* Available only when the send is completed.
*/
amountSpent: z.instanceof(Money).optional(),
amountSpent: z.optional(z.instanceof(Money)),
/**
* The actual Lightning Network fee that was charged after the transaction completed.
* This may be less than the `lightningFeeReserve` if the payment was cheaper than expected.
* The difference between `lightningFeeReserve` and `lightningFee`, along with any overpaid inputs, is returned as change to the user.
* Available only when the send is completed.
*/
lightningFee: z.instanceof(Money).optional(),
lightningFee: z.optional(z.instanceof(Money)),
/**
* The actual fee for the transaction.
* This is the sum of `lightningFee` and `cashuSendFee`.
* Available only when the send is completed.
*/
totalFee: z.instanceof(Money).optional(),
totalFee: z.optional(z.instanceof(Money)),
});

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { z } from 'zod';
import { z } from 'zod/mini';
import { ProofSchema } from '~/lib/cashu';
import { Money } from '~/lib/money';

Expand All @@ -22,7 +22,7 @@ export const CashuSwapReceiveDbDataSchema = z.object({
/**
* The description (memo) of the token being swapped.
*/
tokenDescription: z.string().optional(),
tokenDescription: z.optional(z.string()),
/**
* The amount credited to the account.
*/
Expand Down
10 changes: 5 additions & 5 deletions app/features/agicash-db/json-models/cashu-swap-send-db-data.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { z } from 'zod';
import { z } from 'zod/mini';
import { Money } from '~/lib/money';

/**
Expand Down Expand Up @@ -51,8 +51,8 @@ export const CashuSwapSendDbDataSchema = z.object({
* The swap output amounts used for the send and change.
* Will be defined only when the swap is needed (`inputAmount` is greater than the `amountToSend`).
*/
outputAmounts: z
.object({
outputAmounts: z.optional(
z.object({
/**
* The swap output amounts used for the send.
* Proofs with these amounts are then used to create a token which is shared with the receiver.
Expand All @@ -63,8 +63,8 @@ export const CashuSwapSendDbDataSchema = z.object({
* Proofs with these amounts are then returned to the source account as change.
*/
change: z.array(z.number()),
})
.optional(),
}),
),
});

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { z } from 'zod';
import { z } from 'zod/mini';
import { ProofSchema } from '~/lib/cashu';
import { Money } from '~/lib/money';

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { z } from 'zod';
import { z } from 'zod/mini';

export const SparkAccountDetailsDbDataSchema = z.object({
/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { z } from 'zod';
import { z } from 'zod/mini';
import { Money } from '~/lib/money';
import { CashuTokenMeltDbDataSchema } from './cashu-token-melt-db-data';

Expand All @@ -18,17 +18,17 @@ export const SparkLightningReceiveDbDataSchema = z.object({
/**
* The description of the transaction.
*/
description: z.string().optional(),
description: z.optional(z.string()),
/**
* The payment preimage of the lightning payment.
* Will be set only when the receive quote gets paid.
*/
paymentPreimage: z.string().optional(),
paymentPreimage: z.optional(z.string()),
/**
* The data of the cashu token melted for the receive.
* This will be set only for cashu token receives to spark accounts.
*/
cashuTokenMeltData: CashuTokenMeltDbDataSchema.optional(),
cashuTokenMeltData: z.optional(CashuTokenMeltDbDataSchema),
/**
* The total fee for the transaction.
* For lightning receives this will be zero.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { z } from 'zod';
import { z } from 'zod/mini';
import { Money } from '~/lib/money';

/**
Expand All @@ -23,17 +23,17 @@ export const SparkLightningSendDbDataSchema = z.object({
* This is the amount to send plus the actual fee paid to the lightning network.
* Available only when the send is initiated.
*/
amountSpent: z.instanceof(Money).optional(),
amountSpent: z.optional(z.instanceof(Money)),
/**
* The actual Lightning Network fee that was charged for the transaction.
* Will be set only when the send is initiated.
*/
lightningFee: z.instanceof(Money).optional(),
lightningFee: z.optional(z.instanceof(Money)),
/**
* Preimage of the lightning payment.
* Will be set only when the send is completed.
*/
paymentPreimage: z.string().optional(),
paymentPreimage: z.optional(z.string()),
});

/**
Expand Down
8 changes: 4 additions & 4 deletions app/features/buy/buy-checkout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ import {
type SparkAccount,
getAccountHomePath,
} from '../accounts/account';
import { useCashuReceiveQuote } from '../receive/cashu-receive-quote-hooks';
import { useSparkReceiveQuote } from '../receive/spark-receive-quote-hooks';
import { useTrackCashuReceiveQuote } from '../receive/cashu-receive-quote-hooks';
import { useTrackSparkReceiveQuote } from '../receive/spark-receive-quote-hooks';
import { getDefaultUnit } from '../shared/currencies';
import { MoneyWithConvertedAmount } from '../shared/money-with-converted-amount';
import type { BuyQuote } from './buy-store';
Expand Down Expand Up @@ -225,7 +225,7 @@ export function BuyCheckoutCashu({
const { redirectTo } = useRedirectTo(getAccountHomePath(account));
const navigateToTransaction = useNavigateToTransaction(redirectTo);

const { status: quotePaymentStatus } = useCashuReceiveQuote({
const { status: quotePaymentStatus } = useTrackCashuReceiveQuote({
quoteId: quote.id,
onPaid: (cashuQuote) => {
navigateToTransaction(cashuQuote.transactionId);
Expand Down Expand Up @@ -255,7 +255,7 @@ export function BuyCheckoutSpark({
const { redirectTo } = useRedirectTo(getAccountHomePath(account));
const navigateToTransaction = useNavigateToTransaction(redirectTo);

const { status: quotePaymentStatus } = useSparkReceiveQuote({
const { status: quotePaymentStatus } = useTrackSparkReceiveQuote({
quoteId: quote.id,
onPaid: (sparkQuote) => {
navigateToTransaction(sparkQuote.transactionId);
Expand Down
2 changes: 1 addition & 1 deletion app/features/contacts/contact.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { z } from 'zod';
import { z } from 'zod/mini';

const ContactSchema = z.object({
id: z.string(),
Expand Down
7 changes: 3 additions & 4 deletions app/features/email/welcome-email-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ export async function sendWelcomeEmail(data: {
if (!resendApiKey || !resendWelcomeTemplateId) {
Sentry.captureException(
new Error('Missing RESEND_API_KEY or RESEND_WELCOME_TEMPLATE_ID'),
{ extra: { code: 'server_misconfigured', userId } },
);
return;
}
Expand Down Expand Up @@ -60,8 +59,8 @@ export async function sendWelcomeEmail(data: {
userId,
});
} catch (error) {
Sentry.captureException(error, {
extra: { code: 'email_send_failed', userId },
});
Sentry.captureException(
new Error('Failed to send welcome email', { cause: error }),
);
}
}
Loading
Loading