Skip to content
Open
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
19 changes: 19 additions & 0 deletions apps/api/src/payments/dto/create-payment-intent.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { IsEnum, IsNumber, IsObject, IsOptional, Min } from 'class-validator';
import { Type } from 'class-transformer';
import { Currency } from '../enums/currency.enum.js';

export class CreatePaymentIntentDto {
@IsNumber()
@Min(0.01, { message: 'amount must be at least 0.01' })
@Type(() => Number)
amount!: number;

@IsEnum(Currency, {
message: `currency must be one of: ${Object.values(Currency).join(', ')}`,
})
currency!: Currency;

@IsOptional()
@IsObject()
metadata?: Record<string, unknown>;
}
5 changes: 5 additions & 0 deletions apps/api/src/payments/enums/currency.enum.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export enum Currency {
USDC = 'USDC',
EURC = 'EURC',
XLM = 'XLM',
}
19 changes: 19 additions & 0 deletions apps/api/src/payments/payments.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Body, Controller, HttpCode, HttpStatus, Post } from '@nestjs/common';
import { CurrentMerchant } from '../auth/decorators/current-merchant.decorator.js';
import { type MerchantUser } from '../auth/interfaces/merchant-user.interface.js';
import { CreatePaymentIntentDto } from './dto/create-payment-intent.dto.js';
import { PaymentsService, type PaymentIntent } from './payments.service.js';

@Controller('payments')
export class PaymentsController {
constructor(private readonly paymentsService: PaymentsService) {}

@Post('intents')
@HttpCode(HttpStatus.CREATED)
createPaymentIntent(
@Body() dto: CreatePaymentIntentDto,
@CurrentMerchant() merchant: MerchantUser,
): PaymentIntent {
return this.paymentsService.createPaymentIntent(dto, merchant.merchant_id);
}
}
9 changes: 9 additions & 0 deletions apps/api/src/payments/payments.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Module } from '@nestjs/common';
import { PaymentsController } from './payments.controller.js';
import { PaymentsService } from './payments.service.js';

@Module({
controllers: [PaymentsController],
providers: [PaymentsService],
})
export class PaymentsModule {}
27 changes: 27 additions & 0 deletions apps/api/src/payments/payments.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { Injectable } from '@nestjs/common';
import { type CreatePaymentIntentDto } from './dto/create-payment-intent.dto.js';

export interface PaymentIntent {
id: string;
merchantId: string;
amount: number;
currency: string;
metadata?: Record<string, unknown>;
status: 'pending';
createdAt: string;
}

@Injectable()
export class PaymentsService {
createPaymentIntent(dto: CreatePaymentIntentDto, merchantId: string): PaymentIntent {
return {
id: crypto.randomUUID(),
merchantId,
amount: dto.amount,
currency: dto.currency,
metadata: dto.metadata,
status: 'pending',
createdAt: new Date().toISOString(),
};
}
}
4 changes: 2 additions & 2 deletions apps/frontend/src/app/checkout/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -511,7 +511,7 @@ export default function PaymentCheckout() {
onClick={handleBankPayment}
className="w-full bg-white hover:bg-zinc-100 text-black h-14 rounded-xl mt-6 text-base transition-all duration-200 shadow-lg shadow-white/10"
>
I've Completed the Transfer
I&apos;ve Completed the Transfer
</Button>

<p className="text-xs text-zinc-500 text-center pt-2">
Expand Down Expand Up @@ -611,7 +611,7 @@ export default function PaymentCheckout() {
onClick={handleCryptoDetection}
className="w-full bg-white hover:bg-zinc-100 text-black h-14 rounded-xl mt-6 text-base transition-all duration-200 shadow-lg shadow-white/10"
>
I've Sent the Payment
I&apos;ve Sent the Payment
</Button>

<p className="text-xs text-zinc-500 text-center pt-2">
Expand Down
150 changes: 76 additions & 74 deletions apps/frontend/src/app/dashboard/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client';

import { motion } from "motion/react";
import { motion } from 'motion/react';
import {
TrendingUp,
TrendingDown,
Expand All @@ -10,90 +10,90 @@ import {
Clock,
CheckCircle2,
AlertCircle,
} from "lucide-react";
} from 'lucide-react';

const stats = [
{
label: "Total Volume (30d)",
value: "$12,847,392.45",
change: "+18.2%",
trend: "up",
label: 'Total Volume (30d)',
value: '$12,847,392.45',
change: '+18.2%',
trend: 'up',
icon: DollarSign,
},
{
label: "Settlement Balance",
value: "$2,103,482.12",
change: "+5.4%",
trend: "up",
label: 'Settlement Balance',
value: '$2,103,482.12',
change: '+5.4%',
trend: 'up',
icon: Activity,
},
{
label: "Pending Settlements",
value: "47",
change: "-12.3%",
trend: "down",
label: 'Pending Settlements',
value: '47',
change: '-12.3%',
trend: 'down',
icon: Clock,
},
{
label: "Reserve Ratio",
value: "127.3%",
change: "+2.1%",
trend: "up",
label: 'Reserve Ratio',
value: '127.3%',
change: '+2.1%',
trend: 'up',
icon: CheckCircle2,
},
];

const assets = [
{ symbol: "sUSDC", balance: "1,245,382.45", usd: "1,245,382.45", change: "+2.3%" },
{ symbol: "sBTC", balance: "12.4583", usd: "625,847.92", change: "+5.1%" },
{ symbol: "sETH", balance: "145.2341", usd: "232,251.75", change: "-1.2%" },
{ symbol: 'sUSDC', balance: '1,245,382.45', usd: '1,245,382.45', change: '+2.3%' },
{ symbol: 'sBTC', balance: '12.4583', usd: '625,847.92', change: '+5.1%' },
{ symbol: 'sETH', balance: '145.2341', usd: '232,251.75', change: '-1.2%' },
];

const transactions = [
{
id: "pay_9k2j3n4k5j6h",
type: "Payment",
asset: "sUSDC",
amount: "+12,450.00",
status: "completed",
time: "2m ago",
hash: "0x7a8f9b2c...4e5d6f1a",
id: 'pay_9k2j3n4k5j6h',
type: 'Payment',
asset: 'sUSDC',
amount: '+12,450.00',
status: 'completed',
time: '2m ago',
hash: '0x7a8f9b2c...4e5d6f1a',
},
{
id: "pay_8h1j2k3l4m5n",
type: "Redemption",
asset: "sBTC",
amount: "-0.2341",
status: "completed",
time: "5m ago",
hash: "0x3c4d5e6f...7a8b9c0d",
id: 'pay_8h1j2k3l4m5n',
type: 'Redemption',
asset: 'sBTC',
amount: '-0.2341',
status: 'completed',
time: '5m ago',
hash: '0x3c4d5e6f...7a8b9c0d',
},
{
id: "pay_7g8h9i0j1k2l",
type: "Payment",
asset: "sETH",
amount: "+5.4321",
status: "pending",
time: "8m ago",
hash: "0x1a2b3c4d...5e6f7g8h",
id: 'pay_7g8h9i0j1k2l',
type: 'Payment',
asset: 'sETH',
amount: '+5.4321',
status: 'pending',
time: '8m ago',
hash: '0x1a2b3c4d...5e6f7g8h',
},
{
id: "pay_6f7g8h9i0j1k",
type: "Settlement",
asset: "sUSDC",
amount: "-8,230.50",
status: "completed",
time: "12m ago",
hash: "0x9h8g7f6e...5d4c3b2a",
id: 'pay_6f7g8h9i0j1k',
type: 'Settlement',
asset: 'sUSDC',
amount: '-8,230.50',
status: 'completed',
time: '12m ago',
hash: '0x9h8g7f6e...5d4c3b2a',
},
{
id: "pay_5e6f7g8h9i0j",
type: "Payment",
asset: "sBTC",
amount: "+0.1234",
status: "completed",
time: "15m ago",
hash: "0x2b3c4d5e...6f7g8h9i",
id: 'pay_5e6f7g8h9i0j',
type: 'Payment',
asset: 'sBTC',
amount: '+0.1234',
status: 'completed',
time: '15m ago',
hash: '0x2b3c4d5e...6f7g8h9i',
},
];

Expand All @@ -109,9 +109,7 @@ export default function OverviewPage() {
>
Overview
</motion.h1>
<p className="text-sm text-neutral-400">
Real-time metrics and settlement status
</p>
<p className="text-sm text-neutral-400">Real-time metrics and settlement status</p>
</div>

{/* Stats Grid */}
Expand All @@ -136,25 +134,25 @@ export default function OverviewPage() {
repeatDelay: 2,
}}
/>

<div className="flex items-start justify-between mb-4 relative z-10">
<div className="p-2 bg-white/5 rounded-lg">
<stat.icon className="size-5 text-white" />
</div>
<div
className={`flex items-center gap-1 text-xs ${
stat.trend === "up" ? "text-green-400" : "text-red-400"
stat.trend === 'up' ? 'text-green-400' : 'text-red-400'
}`}
>
{stat.trend === "up" ? (
{stat.trend === 'up' ? (
<TrendingUp className="size-3" />
) : (
<TrendingDown className="size-3" />
)}
{stat.change}
</div>
</div>

<div className="relative z-10">
<div className="text-2xl sm:text-3xl font-medium mb-1">{stat.value}</div>
<div className="text-xs text-neutral-500">{stat.label}</div>
Expand Down Expand Up @@ -200,7 +198,9 @@ export default function OverviewPage() {
</div>
<div className="text-right">
<div className="font-medium">${asset.usd}</div>
<div className={`text-sm ${asset.change.startsWith('+') ? 'text-green-400' : 'text-red-400'}`}>
<div
className={`text-sm ${asset.change.startsWith('+') ? 'text-green-400' : 'text-red-400'}`}
>
{asset.change}
</div>
</div>
Expand All @@ -211,7 +211,7 @@ export default function OverviewPage() {
<motion.div
className="h-full bg-gradient-to-r from-white/30 to-white/10"
initial={{ width: 0 }}
animate={{ width: `${Math.random() * 40 + 60}%` }}
animate={{ width: `${[85, 72, 64][index % 3]}%` }}
transition={{ duration: 1, delay: 0.5 + index * 0.1 }}
/>
</div>
Expand All @@ -228,7 +228,7 @@ export default function OverviewPage() {
transition={{ delay: 0.3 }}
>
<h2 className="text-lg font-medium mb-6">Reserve Health</h2>

<div className="flex items-center justify-center mb-6">
<div className="relative w-40 h-40">
{/* Background circle */}
Expand All @@ -249,9 +249,9 @@ export default function OverviewPage() {
stroke="rgba(255,255,255,0.3)"
strokeWidth="12"
strokeLinecap="round"
initial={{ strokeDasharray: "0 440" }}
animate={{ strokeDasharray: "350 440" }}
transition={{ duration: 1.5, ease: "easeOut" }}
initial={{ strokeDasharray: '0 440' }}
animate={{ strokeDasharray: '350 440' }}
transition={{ duration: 1.5, ease: 'easeOut' }}
/>
</svg>
<div className="absolute inset-0 flex flex-col items-center justify-center">
Expand Down Expand Up @@ -336,18 +336,20 @@ export default function OverviewPage() {
{tx.asset}
</span>
</td>
<td className={`py-3 px-4 font-medium ${tx.amount.startsWith('+') ? 'text-green-400' : 'text-red-400'}`}>
<td
className={`py-3 px-4 font-medium ${tx.amount.startsWith('+') ? 'text-green-400' : 'text-red-400'}`}
>
{tx.amount}
</td>
<td className="py-3 px-4">
<span
className={`inline-flex items-center gap-1 px-2 py-1 rounded text-xs ${
tx.status === "completed"
? "bg-green-400/10 text-green-400"
: "bg-yellow-400/10 text-yellow-400"
tx.status === 'completed'
? 'bg-green-400/10 text-green-400'
: 'bg-yellow-400/10 text-yellow-400'
}`}
>
{tx.status === "completed" ? (
{tx.status === 'completed' ? (
<CheckCircle2 className="size-3" />
) : (
<AlertCircle className="size-3" />
Expand Down