Skip to content

Commit

Permalink
add unlocked eGifts (#200)
Browse files Browse the repository at this point in the history
  • Loading branch information
gudnuf authored Dec 2, 2024
1 parent f1f0eb0 commit 3146884
Show file tree
Hide file tree
Showing 14 changed files with 114 additions and 25 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Token" ALTER COLUMN "token" DROP NOT NULL;
2 changes: 1 addition & 1 deletion prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ model Notification {

model Token {
id String @id
token String
token String?
createdAt DateTime @default(now())
redeemedAt DateTime?
giftId Int?
Expand Down
2 changes: 1 addition & 1 deletion src/components/buttons/Send/SendFlow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,7 @@ const SendFlow = ({ onClose }: SendFlowProps) => {
}
};

const handleSelectContact = (contact: PublicContact) => {
const handleSelectContact = (contact?: PublicContact) => {
if (state.isGiftMode) {
setState({ ...state, step: 'selectGift', contact });
} else {
Expand Down
22 changes: 19 additions & 3 deletions src/components/eGifts/ViewGiftModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,25 @@ interface ViewGiftModalProps {
stickerPath: string;
selectedContact: PublicContact | null;
txid: string;
token?: string;
}

interface ViewGiftModalBodyProps {
amountCents: number;
stickerPath: string;
txid?: string;
token?: string;
}

export const ViewGiftModalBody = ({ amountCents, stickerPath, txid }: ViewGiftModalBodyProps) => {
export const ViewGiftModalBody = ({
amountCents,
stickerPath,
txid,
token,
}: ViewGiftModalBodyProps) => {
const base = `${window.location.origin}/wallet?`;
const toCopy = token ? base + `token=${token}` : base + `txid=${txid}`;

return (
<Modal.Body>
<div className='flex flex-col justify-center items-center text-black text-4xl '>
Expand All @@ -32,7 +42,7 @@ export const ViewGiftModalBody = ({ amountCents, stickerPath, txid }: ViewGiftMo
/>
{txid && (
<ClipboardButton
toCopy={`${window.location.origin}/wallet?txid=${txid}`}
toCopy={toCopy}
toShow={'Share'}
className='btn-primary hover:!bg-[var(--btn-primary-bg)] mt-6'
/>
Expand All @@ -49,6 +59,7 @@ export const ViewGiftModal = ({
stickerPath,
selectedContact,
txid,
token,
}: ViewGiftModalProps) => {
return (
<Modal show={isOpen} onClose={() => onClose()}>
Expand All @@ -60,7 +71,12 @@ export const ViewGiftModal = ({
</a>
</h2>
</Modal.Header>
<ViewGiftModalBody amountCents={amountCents} stickerPath={stickerPath} txid={txid} />
<ViewGiftModalBody
amountCents={amountCents}
stickerPath={stickerPath}
txid={txid}
token={token}
/>
</Modal>
);
};
Expand Down
15 changes: 9 additions & 6 deletions src/components/transactionHistory/HistoryTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const HistoryTable: React.FC<{
const [isSendModalOpen, setIsSendModalOpen] = useState(false);
const [tokenLockedTo, setTokenLockedTo] = useState<PublicContact | null>(null);
const [txid, setTxid] = useState<string | undefined>();
const [token, setToken] = useState<string | undefined>();
const [isViewGiftModalOpen, setIsViewGiftModalOpen] = useState(false);
const [selectedGift, setSelectedGift] = useState<GiftAsset | undefined>(undefined);

Expand All @@ -30,21 +31,22 @@ const HistoryTable: React.FC<{
const openViewGiftModal = async (tx: EcashTransaction & { giftId: number }) => {
setIsViewGiftModalOpen(true);
setIsSendModalOpen(false);
const gift = await getGiftById(tx.giftId);
const gift = getGiftById(tx.giftId);
if (!gift) {
console.error('Gift not found:', tx.gift);
return;
}
setSelectedGift(gift);
/** Begin backwards compatibility for < v0.2.2 */
if (new Date(tx.date).getTime() > 1724352697905) {
setTxid(computeTxId(tx.token));
}
/** End backwards compatibility for < v0.2.2 */
if (tx.pubkey) {
/** Begin backwards compatibility for < v0.2.2 */
if (new Date(tx.date).getTime() > 1724352697905) {
setTxid(computeTxId(tx.token));
}
/** End backwards compatibility for < v0.2.2 */
const contact = await fetchContact(tx.pubkey?.slice(2));
setTokenLockedTo(contact);
}
setToken(tx.token);
};

const openSendEcashModal = async (tx: EcashTransaction) => {
Expand Down Expand Up @@ -94,6 +96,7 @@ const HistoryTable: React.FC<{
selectedContact={tokenLockedTo}
amountCents={selectedGift.amount}
txid={txid}
token={token}
/>
)}
</>
Expand Down
18 changes: 14 additions & 4 deletions src/components/views/SelectContact.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import ContactTableRowItem from '../modals/ContactsModal/ContactTableRowItem';
import useContacts from '@/hooks/boardwalk/useContacts';
import { UserPlusIcon } from '@heroicons/react/20/solid';
import { ArrowRightIcon, UserPlusIcon } from '@heroicons/react/20/solid';
import { Button, Table, TextInput } from 'flowbite-react';
import { useMemo, useState } from 'react';
import { PublicContact } from '@/types';
Expand All @@ -12,7 +12,7 @@ import { contactsTableTheme } from '@/themes/tableThemes';
const SelectContact = ({
onSelectContact,
}: {
onSelectContact: (contact: PublicContact) => void;
onSelectContact: (contact?: PublicContact) => void;
}) => {
const [currentView, setCurrentView] = useState<'select' | 'add'>('select');
const [addingContact, setAddingContact] = useState(false);
Expand Down Expand Up @@ -65,7 +65,7 @@ const SelectContact = ({
return (
<div className='flex flex-col items-center justify-start space-y-4 w-full h-full'>
{currentView === 'select' ? (
<>
<div className='flex flex-col h-full w-full'>
<div className='flex justify-between items-center mb-3 w-full'>
<TextInput
placeholder='Search contacts'
Expand Down Expand Up @@ -96,7 +96,17 @@ const SelectContact = ({
</Table.Body>
</Table>
</div>
</>
<div className='mt-auto self-end'>
<Button
onClick={() => onSelectContact(undefined)}
color='primary'
className='btn-primary'
>
Skip
<ArrowRightIcon className='h-5 w-5' />
</Button>
</div>
</div>
) : (
<form onSubmit={handleAddContact} className='w-full'>
<div>
Expand Down
9 changes: 6 additions & 3 deletions src/hooks/boardwalk/useWallet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ import { Currency, GiftAsset, MintQuoteStateExt, PublicContact } from '@/types';
import { useProofStorage } from '../cashu/useProofStorage';
import { useCashuContext } from '../contexts/cashuContext';
import { setSuccess } from '@/redux/slices/ActivitySlice';
import { postTokenToDb } from '@/utils/appApiRequests';
import { postTokenToDb, postUnlockedGiftToDb } from '@/utils/appApiRequests';
import useNotifications from './useNotifications';
import { MintQuoteState } from '@cashu/cashu-ts';
import { formatUnit } from '@/utils/formatting';
import useMintlessMode from './useMintlessMode';
import { isMintQuoteExpired } from '@/utils/cashu';
import { computeTxId, isMintQuoteExpired } from '@/utils/cashu';
import { useAppDispatch } from '@/redux/store';
import { useCashu } from '../cashu/useCashu';
import { useToast } from '../util/useToast';
Expand Down Expand Up @@ -176,13 +176,16 @@ const useWallet = () => {
if (contact) {
txid = await postTokenToDb(sendableToken, gift?.id);
await sendTokenAsNotification(sendableToken, txid);
} else if (gift) {
txid = computeTxId(sendableToken);
await postUnlockedGiftToDb(txid, gift.id);
}

addToast(
`eGift sent (${formatUnit(gift.amount + (gift?.fee || 0), activeUnit)})`,
'success',
);
return { txid, token: sendableToken };
return { txid: contact ? txid : undefined, token: sendableToken };
} catch (error: any) {
console.error('Error sending token:', error);
const msg = error.message || 'Failed to send token';
Expand Down
13 changes: 13 additions & 0 deletions src/lib/tokenModels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,19 @@ export const createTokenInDb = async (data: PostTokenRequest, txid: string, isFe
return token;
};

export const createUnlockedGift = async (txid: string, giftId: number) => {
const token = await prisma.token.create({
data: {
giftId,
id: txid,
recipientPubkey: null,
isFee: false,
},
});

return token;
};

export const findTokenByTxId = async (txid: string) => {
return await prisma.token.findUnique({
where: {
Expand Down
20 changes: 20 additions & 0 deletions src/pages/api/token/gift.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { createUnlockedGift } from '@/lib/tokenModels';
import { PostTokenResponse, PostUnlockedGiftRequest } from '@/types';
import type { NextApiRequest, NextApiResponse } from 'next';

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method === 'POST') {
try {
const { txid, giftId } = req.body as PostUnlockedGiftRequest;

await createUnlockedGift(txid, Number(giftId));

return res.status(200).json({ txid } as PostTokenResponse);
} catch (error: any) {
return res.status(500).json({ message: error.message });
}
} else {
res.setHeader('Allow', ['POST']);
res.status(405).end(`Method ${req.method} Not Allowed`);
}
}
7 changes: 6 additions & 1 deletion src/pages/api/token/pr/[id]/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,12 @@ export default async function handler(
.status(400)
.json({ error: 'Does not support checking reusable payment requests' });
} else if (paymentRequest.tokens.length > 0) {
token = paymentRequest.tokens[0].token;
const prTok = paymentRequest.tokens[0].token;
if (prTok) {
token = prTok;
} else {
throw new Error('Token does not exist on payment request');
}
}

return res.status(200).json({
Expand Down
2 changes: 1 addition & 1 deletion src/pages/api/token/pr/[id]/last-paid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export default async function handler(
)
: null;

if (!latestToken) {
if (!latestToken || !latestToken.token) {
return res.status(200).json({ token: null, lastPaid: null });
}

Expand Down
16 changes: 12 additions & 4 deletions src/pages/wallet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { useCashu } from '@/hooks/cashu/useCashu';
import { useCashuContext } from '@/hooks/contexts/cashuContext';
import { PublicContact, TokenProps, GiftAsset, Currency } from '@/types';
import { findContactByPubkey, isContactsTrustedMint } from '@/lib/contactModels';
import { proofsLockedTo } from '@/utils/cashu';
import { computeTxId, proofsLockedTo } from '@/utils/cashu';
import { formatUrl, getRequestedDomainFromRequest } from '@/utils/url';
import NotificationDrawer from '@/components/notifications/NotificationDrawer';
import { formatTokenAmount } from '@/utils/formatting';
Expand Down Expand Up @@ -292,19 +292,27 @@ export const getServerSideProps: GetServerSideProps = async (
let token = context.query.token as string;
let giftPath = null;
let gift: GiftAsset | undefined = undefined;
const txid = context.query.txid as string;
const txid = context.query.txid
? (context.query.txid as string)
: token
? computeTxId(token)
: undefined;

const baseRequestUrl = getRequestedDomainFromRequest(context.req);

if (txid && !token) {
if (txid) {
const tokenEntry = await findTokenByTxId(txid);
if (tokenEntry) {
token = tokenEntry.token;
if (tokenEntry.giftId) {
gift = await lookupGiftById(tokenEntry.giftId);
giftPath = gift?.selectedSrc || null;
}
}

if (!token) {
if (!tokenEntry?.token) throw new Error('Token not found');
token = tokenEntry.token;
}
}

let tokenData: TokenProps | null = null;
Expand Down
7 changes: 6 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -257,12 +257,17 @@ export type PostTokenRequest = {
isFee?: boolean;
};

export type PostUnlockedGiftRequest = {
giftId: number;
txid: string;
};

export type PostTokenResponse = {
txid: string;
};

export type GetTokenResponse = {
token: string;
token: string | null;
giftId: number | null;
};

Expand Down
4 changes: 4 additions & 0 deletions src/utils/appApiRequests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,10 @@ export const postTokenToDb = async (token: string, giftId?: number, isFee?: bool
).txid;
};

export const postUnlockedGiftToDb = async (txid: string, giftId: number) => {
return await request<PostTokenResponse>(`/api/token/gift`, 'POST', { txid, giftId });
};

export const getTokenFromDb = async (txid: string) => {
return await request<GetTokenResponse>(`/api/token/${txid}`, 'GET', undefined);
};

0 comments on commit 3146884

Please sign in to comment.