Skip to content
Twilight React Playground
ثيم رائدaren

Wallet

route-moduleBeginnerserverbrowser

The signed-in customer's wallet balance and transaction history, starting from the ?page= in the address and loading more as they scroll.

import { Wallet, WalletPage, walletLoader, WalletPageProps } from '@salla.sa/twilight-theme-engine/routes/account';

In plain words

Stores can keep money for a customer in a wallet: refunds and cashback go in, purchases paid by wallet come out. /account/wallet shows the balance and a table of those transactions.

Signature

const Wallet: {
  readonly id: 'customer.wallet';
  readonly loader: (
    ctx: LoaderContext,   // { search?: { page?: string | number }; locale?: string }
    extend?: (data: WalletPageProps, ctx: { params: {} }) => Record<string, unknown> | Promise<Record<string, unknown>>
  ) => Promise<WalletPageProps>;
  readonly head: (ctx: TwilightContext, data: WalletPageProps) => HeadDescriptor;
  readonly Component: React.LazyExoticComponent<(props: WalletPageProps) => JSX.Element>;
};

const WalletPage: typeof Wallet.Component;
function walletLoader(ctx: LoaderContext): Promise<WalletPageProps>;

interface WalletPageProps {
  page: Page;                        // { title, slug: 'wallet' }
  query: { page: number };
  transactions: TransactionItem[];
  balance: number;
  pagination: RoutePagination;       // only next is set
}

Example

app/routes/account.wallet.tsx (generated)
import { createFileRoute } from '@tanstack/react-router';
import { Wallet } from '@salla.sa/twilight-theme-engine/routes/account';
import type { WalletPageProps } from '@salla.sa/twilight-theme-engine/routes/account';
import { CustomerPageSkeleton } from '@salla.sa/twilight-theme-engine/skeleton';
import { withHead } from '@salla.sa/twilight-theme-engine/tanstack';

export const Route = createFileRoute('/{-$locale}/account/wallet')({
  validateSearch: (search: Record<string, unknown>) => {
    const page = Number(search.page) || 1;
    return page > 1 ? { page } : {};
  },
  loaderDeps: ({ search }) => ({ page: search.page }),
  loader: ({ deps }): Promise<WalletPageProps> =>
    Wallet.loader({ search: { page: deps.page ?? 1 } }),
  head: withHead(Wallet),
  pendingComponent: () => <CustomerPageSkeleton />,
  component: WalletComponent,
});

function WalletComponent() {
  const data: WalletPageProps = Route.useLoaderData();
  return <Wallet.Component {...data} />;
}

How it behaves

  • walletLoader reads Number(ctx.search?.page) || 1 and wraps the API call in orUnauthorized. The generated route passes no locale; none is needed.

  • WalletPage formats the balance and amounts with useMoney (type general), loads further transactions in the browser as the customer scrolls, and renders the slots customer:wallet.transactions.start and customer:wallet.transactions.end.

  • head sets the title and hreflang alternates only.

Gotchas

  • A guest sees a 400 page, not the 401 page: Salla answers the wallet request without a customer token with 400 token_not_provided, which orUnauthorized passes on (see Orders).

  • Transaction dates are formatted while rendering with new Date(created_at * 1000).toLocaleDateString(locale, { …, hour: 'numeric' }), which uses the time zone of whatever runs the code. The server renders in UTC; a browser in another time zone can print a different hour or day, and React reports a hydration mismatch.

Related

Source and docs