Skip to content

Commit b9ddb38

Browse files
committed
feat(cards): add QR code generation for shared cards
1 parent 1f183ab commit b9ddb38

2 files changed

Lines changed: 81 additions & 0 deletions

File tree

apps/backend/src/routes/cards.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import type { Card } from '@devcard/shared';
77
import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
88
import { CardVisibility } from '@prisma/client';
99
import { hashIp } from '../utils/refreshToken';
10+
import { id } from 'zod/v4/locales';
1011

1112
export interface CreateCardBody {
1213
title: string;
@@ -254,4 +255,37 @@ export async function cardRoutes(app: FastifyInstance): Promise<void> {
254255
handleDbError(error, request,reply)
255256
}
256257
})
258+
259+
app.get('/:id/qr', async(request: FastifyRequest<{Params: {id: string}}>, reply:FastifyReply) => {
260+
const cardId = request.params.id
261+
const userId = request.user.id
262+
263+
try {
264+
const qrImage = await cardService.genrateQr(app, userId, cardId)
265+
return reply.type('image/png').send(qrImage)
266+
} catch (error:any) {
267+
if (error?.code === 'CARD_NOT_FOUND') {
268+
return reply.status(404).send({
269+
error: error.message,
270+
});
271+
}
272+
if (error?.code === 'CARD_PRIVATE') {
273+
return reply.status(403).send({
274+
error: error.message,
275+
});
276+
}
277+
if (error?.code === 'QR_ENABLED') {
278+
return reply.status(404).send({
279+
error: error.message,
280+
});
281+
}
282+
if (error?.code === 'QR_IMAGE') {
283+
return reply.status(403).send({
284+
error: error.message,
285+
});
286+
}
287+
app.log.error(error)
288+
handleDbError(error,request, reply)
289+
}
290+
})
257291
}

apps/backend/src/services/cardService.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { generateUniqueSlug } from '../utils/slug';
44

55
import type { CreateCardBody } from '../routes/cards';
66
import type { FastifyInstance } from 'fastify';
7+
import QRCode from 'qrcode';
78

89
type CardLinkResponse = { platformLink: unknown };
910
type RawCard = { id: string; title: string; isDefault: boolean; cardLinks: CardLinkResponse[] };
@@ -300,4 +301,50 @@ export async function getSharedCard(app:FastifyInstance, slug:string){
300301
}
301302

302303
return card
304+
}
305+
306+
307+
export async function genrateQr(app: FastifyInstance,userId:string, id: string){
308+
const card = await app.prisma.card.findFirst({
309+
where:{
310+
id,
311+
userId
312+
}
313+
})
314+
315+
if (!card) {
316+
throw Object.assign(
317+
new Error('Card not found'),
318+
{ code: 'CARD_NOT_FOUND' }
319+
);
320+
}
321+
322+
323+
if(card?.visibility === CardVisibility.PRIVATE){
324+
throw Object.assign(
325+
new Error('Private cards cannot be shared'),
326+
{ code: 'CARD_PRIVATE' }
327+
);
328+
}
329+
330+
if(!card.qrEnabled){
331+
throw Object.assign(
332+
new Error('QR is not availbled for this card'),
333+
{ code: 'QR_ENABLED' }
334+
);
335+
}
336+
337+
const shareUrl = `${process.env.MOBILE_REDIRECT_URI}/cards/share/${card.slug}`
338+
const qrImage = await QRCode.toBuffer(shareUrl);
339+
340+
if(!qrImage){
341+
throw Object.assign(
342+
new Error('QR generation failed'),
343+
{ code: 'QR_IMAGE' }
344+
);
345+
}
346+
347+
return qrImage;
348+
349+
303350
}

0 commit comments

Comments
 (0)