Skip to content
Draft
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
Binary file added app/assets/star-cards/blockandbean.agi.cash.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added app/assets/star-cards/compass.agi.cash.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added app/assets/star-cards/fake.agi.cash.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added app/assets/star-cards/fake2.agi.cash.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added app/assets/star-cards/fake4.agi.cash.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added app/assets/star-cards/main-homepage-card.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added app/assets/star-cards/pinkowl.agi.cash.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added app/assets/star-cards/shack.agi.cash.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added app/assets/transparent-card.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added app/assets/whitelogo-small.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion app/components/money-display.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ const textVariants = cva('', {
},
});

const symbolVariants = cva('', {
const symbolVariants = cva('ml-[-0.025em] inline-block leading-none', {
variants: {
size: {
sm: 'text-[1.33rem]',
Expand Down
13 changes: 8 additions & 5 deletions app/components/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { ChevronLeft, X } from 'lucide-react';
import React from 'react';
import { useLocation } from 'react-router';
import {
LinkWithViewTransition,
type ViewTransitionLinkProps,
Expand All @@ -14,7 +15,7 @@ export function Page({ children, className, ...props }: PageProps) {
return (
<div
className={cn(
'mx-auto flex h-dvh w-full flex-col p-4 font-primary sm:items-center sm:px-6 lg:px-8',
'mx-auto flex h-dvh w-full flex-col font-primary sm:items-center sm:px-6 lg:px-8',
className,
)}
{...props}
Expand All @@ -27,8 +28,10 @@ export function Page({ children, className, ...props }: PageProps) {
interface ClosePageButtonProps extends ViewTransitionLinkProps {}

export function ClosePageButton({ className, ...props }: ClosePageButtonProps) {
const location = useLocation();
const redirectTo = new URLSearchParams(location.search).get('redirectTo');
return (
<LinkWithViewTransition {...props}>
<LinkWithViewTransition {...props} to={redirectTo || props.to}>
<X />
</LinkWithViewTransition>
);
Expand Down Expand Up @@ -83,7 +86,7 @@ export function PageHeader({ children, className, ...props }: PageHeaderProps) {

return (
<header
className={cn('mb-4 flex w-full items-center justify-between', className)}
className={cn('flex w-full items-center justify-between p-4', className)}
{...props}
>
{/* Close/back button - always on the left */}
Expand Down Expand Up @@ -129,7 +132,7 @@ export function PageContent({
return (
<main
className={cn(
'flex flex-grow flex-col gap-2 p-2 sm:w-full sm:max-w-sm',
'flex flex-grow flex-col gap-2 px-4 sm:w-full sm:max-w-sm',
className,
)}
{...props}
Expand All @@ -147,7 +150,7 @@ export function PageFooter({ children, className, ...props }: PageFooterProps) {
return (
<footer
className={cn(
'flex w-full flex-col items-center gap-2 p-2 sm:max-w-sm',
'flex w-full flex-col items-center gap-2 p-4 pt-2 sm:max-w-sm',
className,
)}
{...props}
Expand Down
38 changes: 25 additions & 13 deletions app/components/ui/scroll-area.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,36 @@ type ScrollAreaProps = React.ComponentPropsWithoutRef<
typeof ScrollAreaPrimitive.Root
> & {
hideScrollbar?: boolean;
orientation?: 'vertical' | 'horizontal';
};

const ScrollArea = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
ScrollAreaProps
>(({ className, children, hideScrollbar = false, ...props }, ref) => (
<ScrollAreaPrimitive.Root
ref={ref}
className={cn('relative overflow-hidden', className)}
{...props}
>
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar hideScrollbar={hideScrollbar} />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
));
>(
(
{
className,
children,
hideScrollbar = false,
orientation = 'vertical',
...props
},
ref,
) => (
<ScrollAreaPrimitive.Root
ref={ref}
className={cn('relative overflow-hidden', className)}
{...props}
>
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar hideScrollbar={hideScrollbar} orientation={orientation} />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
),
);
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName;

const ScrollBar = React.forwardRef<
Expand Down
45 changes: 35 additions & 10 deletions app/features/accounts/account-hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
useSuspenseQuery,
} from '@tanstack/react-query';
import { useCallback, useMemo, useRef } from 'react';
import { useSearchParams } from 'react-router';
import { type Currency, Money } from '~/lib/money';
import type { AgicashDbAccount } from '../agicash-db/database';
import { useUser } from '../user/user-hooks';
Expand Down Expand Up @@ -251,11 +252,14 @@ export function useAccounts<T extends AccountType = AccountType>(select?: {
currency?: Currency;
type?: T;
isOnline?: boolean;
excludeStarAccounts?: boolean;
starAccountsOnly?: boolean;
}): UseSuspenseQueryResult<ExtendedAccount<T>[]> {
const user = useUser();
const accountRepository = useAccountRepository();

const { currency, type, isOnline } = select ?? {};
const { currency, type, isOnline, excludeStarAccounts, starAccountsOnly } =
select ?? {};

return useSuspenseQuery({
...accountsQueryOptions({ userId: user.id, accountRepository }),
Expand All @@ -280,13 +284,19 @@ export function useAccounts<T extends AccountType = AccountType>(select?: {
if (isOnline !== undefined && account.isOnline !== isOnline) {
return false;
}
if (excludeStarAccounts && account.isStarAccount) {
return false;
}
if (starAccountsOnly && !account.isStarAccount) {
return false;
}
return true;
},
);

return filteredData;
},
[currency, type, isOnline, user],
[currency, type, isOnline, excludeStarAccounts, starAccountsOnly, user],
),
});
}
Expand Down Expand Up @@ -409,15 +419,18 @@ export function useAddCashuAccount() {
return mutateAsync;
}

/**
* @returns the total balance of all accounts for the given currency excluding Star accounts.
*/
export function useBalance(currency: Currency) {
const { data: accounts } = useAccounts({ currency });
const balance = accounts.reduce(
(acc, account) => {
const accountBalance = getAccountBalance(account);
return acc.add(accountBalance);
},
new Money({ amount: 0, currency }),
);
const { data: accounts } = useAccounts({
currency,
excludeStarAccounts: true,
});
const balance = accounts.reduce((acc, account) => {
const accountBalance = getAccountBalance(account);
return acc.add(accountBalance);
}, Money.zero(currency));
return balance;
}

Expand All @@ -437,3 +450,15 @@ export function useSelectItemsWithOnlineAccount() {
[accountsCache],
);
}

/**
* Returns the account specified by the account ID in the URL.
* @param select - The type of the account to get.
*/
export function useGetAccountFromLocation(select?: { type?: AccountType }) {
const [searchParams] = useSearchParams();
const accountId = searchParams.get('accountId');
const { data: accounts } = useAccounts({ type: select?.type });
const account = accounts.find((account) => account.id === accountId);
return account;
}
8 changes: 5 additions & 3 deletions app/features/accounts/account-icons.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import { LandmarkIcon, Zap } from 'lucide-react';
import { LandmarkIcon, StarIcon, Zap } from 'lucide-react';
import type { ReactNode } from 'react';
import type { AccountType } from './account';

const CashuIcon = () => <LandmarkIcon className="h-4 w-4" />;
const NWCIcon = () => <Zap className="h-4 w-4" />;
const StarsIcon = () => <StarIcon className="h-4 w-4" />;

const iconsByAccountType: Record<AccountType, ReactNode> = {
const iconsByAccountType: Record<AccountType | 'star', ReactNode> = {
cashu: <CashuIcon />,
nwc: <NWCIcon />,
star: <StarsIcon />,
};

export function AccountTypeIcon({ type }: { type: AccountType }) {
export function AccountTypeIcon({ type }: { type: AccountType | 'star' }) {
return iconsByAccountType[type];
}
15 changes: 10 additions & 5 deletions app/features/accounts/account-repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
import { type QueryClient, useQueryClient } from '@tanstack/react-query';
import type { DistributedOmit } from 'type-fest';
import {
type MintInfo,
type ExtendedMintInfo,
getCashuProtocolUnit,
getCashuUnit,
getCashuWallet,
Expand Down Expand Up @@ -167,7 +167,7 @@ export class AccountRepository {
proofs: string;
};

const { wallet, isOnline } = await this.getPreloadedWallet(
const { wallet, isOnline, isStarAccount } = await this.getPreloadedWallet(
details.mint_url,
data.currency,
);
Expand All @@ -181,6 +181,7 @@ export class AccountRepository {
keysetCounters: details.keyset_counters,
proofs: await this.encryption.decrypt<Proof[]>(details.proofs),
wallet,
isStarAccount,
} as T;
}

Expand All @@ -199,7 +200,7 @@ export class AccountRepository {
private async getPreloadedWallet(mintUrl: string, currency: Currency) {
const seed = await this.getCashuWalletSeed?.();

let mintInfo: MintInfo;
let mintInfo: ExtendedMintInfo;
let allMintKeysets: MintAllKeysets;
let mintActiveKeys: MintActiveKeys;

Expand Down Expand Up @@ -231,7 +232,7 @@ export class AccountRepository {
unit: getCashuUnit(currency),
bip39seed: seed ?? undefined,
});
return { wallet, isOnline: false };
return { wallet, isOnline: false, isStarAccount: undefined };
}
throw error;
}
Expand Down Expand Up @@ -266,7 +267,11 @@ export class AccountRepository {
// The constructor does not set the keysetId, so we need to set it manually
wallet.keysetId = activeKeyset.id;

return { wallet, isOnline: true };
return {
wallet,
isOnline: true,
isStarAccount: mintInfo.internalMeltsOnly,
};
}
}

Expand Down
2 changes: 1 addition & 1 deletion app/features/accounts/account-selector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ function AccountItem({ account }: { account: AccountSelectorOption }) {

return (
<div className="flex w-full items-center gap-4 px-3 py-4">
<AccountTypeIcon type={account.type} />
<AccountTypeIcon type={account.isStarAccount ? 'star' : account.type} />
<div className="flex w-full flex-col justify-between gap-2 text-start">
<span className="font-medium">{account.name}</span>
<div className="flex items-center justify-between text-xs">
Expand Down
1 change: 1 addition & 0 deletions app/features/accounts/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export type Account = {
name: string;
type: AccountType;
isOnline: boolean;
isStarAccount?: boolean;
currency: Currency;
createdAt: string;
/**
Expand Down
28 changes: 15 additions & 13 deletions app/features/accounts/default-currency-switcher.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { ChevronDown } from 'lucide-react';
import { useState } from 'react';
import {
Drawer,
Expand All @@ -7,7 +6,6 @@ import {
DrawerTitle,
DrawerTrigger,
} from '~/components/ui/drawer';
import { RadioGroup, RadioGroupItem } from '~/components/ui/radio-group';
import { Separator } from '~/components/ui/separator';
import { useToast } from '~/hooks/use-toast';
import type { Currency } from '~/lib/money/types';
Expand Down Expand Up @@ -51,15 +49,21 @@ function CurrencyOption({ data, isSelected, onSelect }: CurrencyOptionProps) {
<span>{label}</span>
<MoneyWithConvertedAmount money={balance} variant="inline" />
</div>
<RadioGroup value={isSelected ? currency : undefined}>
<RadioGroupItem className="text-foreground" value={currency} />
</RadioGroup>
<div className="aspect-square h-4 w-4 rounded-full border border-primary ring-offset-background">
{isSelected && (
<div className="flex h-full w-full items-center justify-center rounded-full bg-primary">
<div className="h-2 w-2 rounded-full bg-primary-foreground" />
</div>
)}
</div>
</button>
);
}

/** A drawer that allows the user to switch their default currency */
export function DefaultCurrencySwitcher() {
export function DefaultCurrencySwitcher({
children,
}: { children: React.ReactNode }) {
const { toast } = useToast();
const [isOpen, setIsOpen] = useState(false);
const defaultCurrency = useUser((user) => user.defaultCurrency);
Expand All @@ -80,13 +84,11 @@ export function DefaultCurrencySwitcher() {

return (
<Drawer open={isOpen} onOpenChange={setIsOpen}>
<DrawerTrigger asChild>
<button type="button" className="flex items-center gap-1">
{defaultCurrency}
<ChevronDown className="h-4 w-4" />
</button>
</DrawerTrigger>
<DrawerContent className="pb-14 font-primary">
<DrawerTrigger asChild>{children}</DrawerTrigger>
<DrawerContent
className="pb-14 font-primary"
aria-describedby={undefined}
>
<div className="mx-auto w-full max-w-sm">
<DrawerHeader>
<DrawerTitle>Select Currency</DrawerTitle>
Expand Down
Loading