Refactor: Fully migrated to direct PostgreSQL, implemented Public API v1, fixed Vercel deployment conflicts, and updated documentation
This commit is contained in:
137
README.md
137
README.md
@@ -1,36 +1,131 @@
|
|||||||
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
|
# 🚀 Pay2Gateway: Hibrit Kripto & Geleneksel Ödeme Geçidi Altyapısı
|
||||||
|
|
||||||
## Getting Started
|
Pay2Gateway, modern e-ticaret siteleri ve dijital platformlar için geliştirilmiş, **Next.js 15** ve **PostgreSQL** tabanlı, hibrit bir ödeme geçidi (Payment Gateway) çözümüdür. Sistem, geleneksel ödeme sağlayıcılarını (Stripe vb.) on-chain kripto ödemeleriyle birleştirerek firmalara tam otomatik ve esnek bir tahsilat altyapısı sunar.
|
||||||
|
|
||||||
First, run the development server:
|
---
|
||||||
|
|
||||||
|
## 🌟 Öne Çıkan Özellikler
|
||||||
|
|
||||||
|
### 🛡️ Kurumsal API Yönetimi (v1)
|
||||||
|
* **API Key Yetkilendirme:** Firmalar (Merchants) için sunucu taraflı güvenli erişim.
|
||||||
|
* **Checkout Sessions:** Fiyat manipülasyonunu engelleyen, tek kullanımlık güvenli ödeme oturumları.
|
||||||
|
* **Gelişmiş Webhook'lar:** Ödeme tamamlandığında veya süpürme (sweep) işlemi gerçekleştiğinde otomatik JSON bildirimleri.
|
||||||
|
|
||||||
|
### ⛓️ Çoklu Zincir (Multi-Chain) Desteği
|
||||||
|
* **EVM Desteği:** Ethereum, Polygon ve BSC ağlarında işlem yapabilme.
|
||||||
|
* **Solana Desteği:** SOL ve SPL tokenlar (USDC vb.) için tam entegrasyon.
|
||||||
|
* **Otomatik Süpürme (Auto-Sweep):** Toplanan fonların ana platform cüzdanına saniyeler içinde otomatik olarak aktarılması.
|
||||||
|
|
||||||
|
### 💰 Dinamik Token & Fiyatlandırma
|
||||||
|
* **Top 20 Token:** CoinMarketCap listesindeki en hacimli 20 kripto para birimi desteği.
|
||||||
|
* **Binance API Entegrasyonu:** Gerçek zamanlı TRY/USD kuru ve token fiyat dönüşümleri.
|
||||||
|
* **Merkezi Konfigürasyon:** `lib/crypto-config.json` üzerinden anında yeni ağ veya token ekleme kolaylığı.
|
||||||
|
|
||||||
|
### 📊 Yönetim Panelleri
|
||||||
|
* **Admin Dashboard:** Tüm sistem istatistikleri, toplam ciro, işlem başarı oranları ve müşteri analitiği.
|
||||||
|
* **Merchant (Firma) Paneli:** İşlem listesi, API anahtarı yönetimi, webhook yapılandırması ve teknik entegrasyon rehberi.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛠️ Teknoloji Yığını
|
||||||
|
|
||||||
|
* **Frontend & API:** [Next.js 15](https://nextjs.org/) (App Router)
|
||||||
|
* **Veritabanı:** PostgreSQL (Direct connectivity via `pg`)
|
||||||
|
* **Blockchain:**
|
||||||
|
* [Ethers.js](https://docs.ethers.org/v6/) (EVM)
|
||||||
|
* [@solana/web3.js](https://solana-labs.github.io/solana-web3.js/) (Solana)
|
||||||
|
* **Styling:** Modern Vanilla CSS & Tailwind (Hibrit)
|
||||||
|
* **İkon Seti:** Lucide React
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Hızlı Kurulum
|
||||||
|
|
||||||
|
### 1. Depoyu Klonlayın
|
||||||
```bash
|
```bash
|
||||||
npm run dev
|
git clone https://github.com/mstfyldz/Pay2Gateway.git
|
||||||
# or
|
cd Pay2Gateway
|
||||||
yarn dev
|
|
||||||
# or
|
|
||||||
pnpm dev
|
|
||||||
# or
|
|
||||||
bun dev
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
### 2. Bağımlılıkları Yükleyin
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
### 3. Ortam Değişkenlerini Yapılandırın
|
||||||
|
`.env` dosyasını oluşturun ve aşağıdaki bilgileri girin:
|
||||||
|
|
||||||
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
|
```env
|
||||||
|
DATABASE_URL=postgres://user:pass@host:5432/db
|
||||||
|
NEXT_PUBLIC_BASE_URL=http://localhost:3000
|
||||||
|
NEXT_PUBLIC_USE_MOCK_PAYMENTS=true # Test için true kalsın
|
||||||
|
|
||||||
## Learn More
|
# Kripto Platform Cüzdanları (Süpürme Hedefi)
|
||||||
|
SOL_PLATFORM_ADDRESS=...
|
||||||
|
EVM_PLATFORM_ADDRESS=...
|
||||||
|
|
||||||
To learn more about Next.js, take a look at the following resources:
|
# Gaz Yakıt Cüzdanı (Fonları süpürmek için gerekli gaz ücreti)
|
||||||
|
CRYPTO_GAS_TANK_KEY=0x...
|
||||||
|
```
|
||||||
|
|
||||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
### 4. Geliştirme Modunu Başlatın
|
||||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
|
---
|
||||||
|
|
||||||
## Deploy on Vercel
|
## 📡 API Kullanımı (v1)
|
||||||
|
|
||||||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
### Ödeme Oturumu Başlatma
|
||||||
|
Firmalar kendi sunucularından şu uç noktaya istek atarak bir ödeme oturumu başlatabilirler.
|
||||||
|
|
||||||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
|
**Request:**
|
||||||
|
- **URL:** `POST /api/v1/checkout`
|
||||||
|
- **Headers:** `x-api-key: YOUR_MERCHANT_API_KEY`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"amount": 250.00,
|
||||||
|
"currency": "TRY",
|
||||||
|
"order_id": "OD_12345",
|
||||||
|
"customer_name": "John Doe",
|
||||||
|
"success_url": "https://mysite.com/success",
|
||||||
|
"cancel_url": "https://mysite.com/cancel"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"id": "uuid-transaction-id",
|
||||||
|
"checkout_url": "http://localhost:3000/checkout?session_id=uuid-transaction-id",
|
||||||
|
"status": "pending"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📂 Dosya Yapısı
|
||||||
|
|
||||||
|
* `/app/api/v1` - Dışarıya açık profesyonel API endpoints.
|
||||||
|
* `/app/admin` - Merkezi yönetim paneli.
|
||||||
|
* `/app/merchant` - Firma özel dashboard ve ayarlar.
|
||||||
|
* `/lib/crypto-engine.ts` - On-chain ana motor (Doğrulama, Süpürme).
|
||||||
|
* `/lib/crypto-config.json` - Desteklenen ağlar ve token listesi.
|
||||||
|
* `/lib/db.ts` - PostgreSQL bağlantı havuzu.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔐 Güvenlik Politikası
|
||||||
|
|
||||||
|
1. **Private Key Yönetimi:** Geçici cüzdan anahtarları şifrelenmiş olarak işlem metadata'sında saklanır ve fon süpürüldükten sonra işlevini yitirir.
|
||||||
|
2. **Fiyat Güvenliği:** Ödeme oturumları (`session_id`) sunucu taraflı oluşturulur; istemci tarafında tutar değişikliği yapılamaz.
|
||||||
|
3. **Hız Sınırı (Rate Limiting):** API istekleri anahtar tabanlı olarak sunucu tarafında izlenir.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
⭐ **Pay2Gateway** - *Geleceğin Ödeme Altyapısı.*
|
||||||
|
|||||||
@@ -9,19 +9,17 @@ import {
|
|||||||
Smartphone,
|
Smartphone,
|
||||||
Calendar
|
Calendar
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { supabaseAdmin } from '@/lib/supabase-admin';
|
import { db } from '@/lib/db';
|
||||||
import { format, subDays } from 'date-fns';
|
import { format, subDays } from 'date-fns';
|
||||||
import { tr } from 'date-fns/locale';
|
import { tr } from 'date-fns/locale';
|
||||||
import AnalyticsBarChart from '@/components/admin/AnalyticsBarChart';
|
import AnalyticsBarChart from '@/components/admin/AnalyticsBarChart';
|
||||||
import QueryRangeSelector from '@/components/admin/QueryRangeSelector';
|
import QueryRangeSelector from '@/components/admin/QueryRangeSelector';
|
||||||
|
|
||||||
async function getAnalyticsData(rangeDays: number = 12) {
|
async function getAnalyticsData(rangeDays: number = 12) {
|
||||||
const { data: transactions, error } = await supabaseAdmin
|
const result = await db.query('SELECT * FROM transactions ORDER BY created_at ASC');
|
||||||
.from('transactions')
|
const transactions = result.rows;
|
||||||
.select('*')
|
|
||||||
.order('created_at', { ascending: true });
|
|
||||||
|
|
||||||
if (error || !transactions) return null;
|
if (!transactions) return null;
|
||||||
|
|
||||||
const successfulTransactions = transactions.filter(t => t.status === 'succeeded');
|
const successfulTransactions = transactions.filter(t => t.status === 'succeeded');
|
||||||
const totalRevenue = successfulTransactions.reduce((acc, t) => acc + Number(t.amount), 0);
|
const totalRevenue = successfulTransactions.reduce((acc, t) => acc + Number(t.amount), 0);
|
||||||
|
|||||||
@@ -8,17 +8,15 @@ import {
|
|||||||
MoreHorizontal,
|
MoreHorizontal,
|
||||||
ArrowUpRight
|
ArrowUpRight
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { supabaseAdmin } from '@/lib/supabase-admin';
|
import { db } from '@/lib/db';
|
||||||
|
|
||||||
import CustomerSearch from '@/components/admin/CustomerSearch';
|
import CustomerSearch from '@/components/admin/CustomerSearch';
|
||||||
|
|
||||||
async function getFilteredCustomers(queryText?: string) {
|
async function getFilteredCustomers(queryText?: string) {
|
||||||
const { data: transactions, error } = await supabaseAdmin
|
const result = await db.query('SELECT * FROM transactions ORDER BY created_at DESC');
|
||||||
.from('transactions')
|
const transactions = result.rows;
|
||||||
.select('*')
|
|
||||||
.order('created_at', { ascending: false });
|
|
||||||
|
|
||||||
if (error || !transactions) return null;
|
if (!transactions) return null;
|
||||||
|
|
||||||
// Group transactions by name or phone
|
// Group transactions by name or phone
|
||||||
const customerMap = new Map();
|
const customerMap = new Map();
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { supabaseAdmin } from '@/lib/supabase-admin';
|
import { db } from '@/lib/db';
|
||||||
import {
|
import {
|
||||||
TrendingUp,
|
TrendingUp,
|
||||||
TrendingDown,
|
TrendingDown,
|
||||||
@@ -15,12 +15,10 @@ import TransactionChart from '@/components/admin/TransactionChart';
|
|||||||
import QueryRangeSelector from '@/components/admin/QueryRangeSelector';
|
import QueryRangeSelector from '@/components/admin/QueryRangeSelector';
|
||||||
|
|
||||||
async function getStats(rangeDays: number = 30) {
|
async function getStats(rangeDays: number = 30) {
|
||||||
const { data: transactions, error } = await supabaseAdmin
|
const result = await db.query('SELECT * FROM transactions ORDER BY created_at DESC');
|
||||||
.from('transactions')
|
const transactions = result.rows;
|
||||||
.select('*')
|
|
||||||
.order('created_at', { ascending: false });
|
|
||||||
|
|
||||||
if (error || !transactions) return null;
|
if (!transactions) return null;
|
||||||
|
|
||||||
const successfulTransactions = transactions.filter(t => t.status === 'succeeded');
|
const successfulTransactions = transactions.filter(t => t.status === 'succeeded');
|
||||||
const totalRevenue = successfulTransactions.reduce((acc, t) => acc + Number(t.amount), 0);
|
const totalRevenue = successfulTransactions.reduce((acc, t) => acc + Number(t.amount), 0);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
import { db } from '@/lib/db';
|
import { db } from '@/lib/db';
|
||||||
import { PaymentProviderFactory } from '@/lib/payment-providers';
|
import { PaymentProviderFactory } from '@/lib/payment-providers';
|
||||||
|
import { CryptoEngine } from '@/lib/crypto-engine';
|
||||||
|
|
||||||
export async function POST(req: NextRequest) {
|
export async function POST(req: NextRequest) {
|
||||||
try {
|
try {
|
||||||
@@ -23,7 +24,7 @@ export async function POST(req: NextRequest) {
|
|||||||
} else {
|
} else {
|
||||||
result = await db.query('SELECT * FROM merchants WHERE short_id = $1', [merchant_id]);
|
result = await db.query('SELECT * FROM merchants WHERE short_id = $1', [merchant_id]);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result.rows.length === 0) {
|
if (result.rows.length === 0) {
|
||||||
return NextResponse.json({ error: 'Firma bulunamadı.' }, { status: 404 });
|
return NextResponse.json({ error: 'Firma bulunamadı.' }, { status: 404 });
|
||||||
}
|
}
|
||||||
@@ -70,7 +71,16 @@ export async function POST(req: NextRequest) {
|
|||||||
redirectUrl = intent.redirectUrl || '';
|
redirectUrl = intent.redirectUrl || '';
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Log transaction in Supabase
|
// 3. Generate Temporary Wallets for Crypto fallback
|
||||||
|
const evmWallet = await CryptoEngine.createTemporaryWallet('POLYGON');
|
||||||
|
const solWallet = await CryptoEngine.createTemporaryWallet('SOLANA');
|
||||||
|
|
||||||
|
const cryptoWallets = {
|
||||||
|
EVM: { address: evmWallet.address, privateKey: evmWallet.privateKey },
|
||||||
|
SOLANA: { address: solWallet.address, privateKey: solWallet.privateKey }
|
||||||
|
};
|
||||||
|
|
||||||
|
// 4. Log transaction in Supabase
|
||||||
try {
|
try {
|
||||||
await db.query(`
|
await db.query(`
|
||||||
INSERT INTO transactions (
|
INSERT INTO transactions (
|
||||||
@@ -81,7 +91,11 @@ export async function POST(req: NextRequest) {
|
|||||||
`, [
|
`, [
|
||||||
amount, currency, 'pending', providerTxId, ref_id,
|
amount, currency, 'pending', providerTxId, ref_id,
|
||||||
customer_name, customer_phone, callback_url, resolvedMerchantId,
|
customer_name, customer_phone, callback_url, resolvedMerchantId,
|
||||||
provider, JSON.stringify({ nextAction, redirectUrl })
|
provider, JSON.stringify({
|
||||||
|
nextAction,
|
||||||
|
redirectUrl,
|
||||||
|
wallets: cryptoWallets
|
||||||
|
})
|
||||||
]);
|
]);
|
||||||
} catch (dbError) {
|
} catch (dbError) {
|
||||||
console.error('Database log error:', dbError);
|
console.error('Database log error:', dbError);
|
||||||
@@ -91,7 +105,11 @@ export async function POST(req: NextRequest) {
|
|||||||
clientSecret: clientSecret,
|
clientSecret: clientSecret,
|
||||||
nextAction,
|
nextAction,
|
||||||
redirectUrl,
|
redirectUrl,
|
||||||
provider
|
provider,
|
||||||
|
wallets: {
|
||||||
|
EVM: evmWallet.address,
|
||||||
|
SOLANA: solWallet.address
|
||||||
|
}
|
||||||
});
|
});
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error('Internal Error:', err);
|
console.error('Internal Error:', err);
|
||||||
|
|||||||
@@ -5,59 +5,110 @@ import { db } from '@/lib/db';
|
|||||||
export async function POST(request: Request) {
|
export async function POST(request: Request) {
|
||||||
try {
|
try {
|
||||||
const body = await request.json();
|
const body = await request.json();
|
||||||
const { txId, merchantAddress, amount, currency } = body;
|
const { txId, network, token } = body;
|
||||||
|
|
||||||
console.log(`[API] Checking and Sweeping for Transaction: ${txId}`);
|
if (!txId) {
|
||||||
|
return NextResponse.json({ success: false, error: "Transaction ID is required" }, { status: 400 });
|
||||||
// This is a demo integration. In a real application:
|
}
|
||||||
// 1. We would look up the transaction ID from the DB
|
|
||||||
// 2. Fetch the temporary wallet private key created for that specific TX
|
|
||||||
// 3. We use the platform address defined in our .env or settings
|
|
||||||
|
|
||||||
// For this demo, we'll use the user's devnet wallet as both the source (temp wallet) and platform
|
const selectedNetwork = network || 'POLYGON';
|
||||||
const demoTempWalletPrivKey = "3Ab6AyfDDWquPJ6ySjHmQmiW3USg7CuDxJSNtrNQySsXj5v4KfBKcw9vnK1Rrfwm6RYq43PdKjiNZekgtNzGsNm2";
|
const selectedToken = token || 'USDT';
|
||||||
const platformAddress = "5pLH1tqZhx8p8WpZ18yr28N42KXB3FXVPzZ9ceCtpBVe";
|
|
||||||
|
|
||||||
// Ensure we have a merchant address or use a fallback for testing
|
|
||||||
const targetMerchant = merchantAddress || "5pLH1tqZhx8p8WpZ18yr28N42KXB3FXVPzZ9ceCtpBVe"; // using same for demo
|
|
||||||
|
|
||||||
// Initialize Crypto Engine for Solana
|
|
||||||
const cryptoEngine = new CryptoEngine('SOLANA');
|
|
||||||
|
|
||||||
console.log("Starting Sweep Process on SOLANA DEVNET...");
|
console.log(`[API] Processing sweep for TX: ${txId} on ${selectedNetwork} with ${selectedToken}`);
|
||||||
|
|
||||||
|
// 1. Fetch the transaction from DB to get the temporary wallet private key
|
||||||
|
const result = await db.query('SELECT * FROM transactions WHERE stripe_pi_id = $1', [txId]);
|
||||||
|
|
||||||
|
if (result.rows.length === 0) {
|
||||||
|
return NextResponse.json({ success: false, error: "Transaction not found" }, { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const transaction = result.rows[0];
|
||||||
|
|
||||||
|
// 1.1 Check if already processed
|
||||||
|
if (transaction.status === 'succeeded') {
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
message: "Transaction already processed",
|
||||||
|
status: 'succeeded'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const metadata = transaction.metadata || {};
|
||||||
|
const wallets = metadata.wallets || {};
|
||||||
|
|
||||||
|
// 2. Determine which wallet to use (EVM or SOLANA)
|
||||||
|
const walletType = selectedNetwork === 'SOLANA' ? 'SOLANA' : 'EVM';
|
||||||
|
const tempWalletConfig = wallets[walletType];
|
||||||
|
|
||||||
|
if (!tempWalletConfig || !tempWalletConfig.privateKey) {
|
||||||
|
return NextResponse.json({ success: false, error: `No temporary wallet found for ${walletType}` }, { status: 500 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Define Platform Address (In production, load from env/settings)
|
||||||
|
const platformAddress = selectedNetwork === 'SOLANA'
|
||||||
|
? process.env.SOL_PLATFORM_ADDRESS || "5pLH1tqZhx8p8WpZ18yr28N42KXB3FXVPzZ9ceCtpBVe"
|
||||||
|
: process.env.EVM_PLATFORM_ADDRESS || "0x70997970C51812dc3A010C7d01b50e0d17dc79C8";
|
||||||
|
|
||||||
|
// 4. Define Merchant Address (Fetch from transaction's merchant)
|
||||||
|
const merchantResult = await db.query('SELECT * FROM merchants WHERE id = $1', [transaction.merchant_id]);
|
||||||
|
const merchantAddress = merchantResult.rows[0]?.wallet_address || platformAddress;
|
||||||
|
|
||||||
|
// 5. Initialize Engine and Verify Payment first
|
||||||
|
const cryptoEngine = new CryptoEngine(selectedNetwork);
|
||||||
|
|
||||||
// Attempt the sweep (this will do a real devnet transaction if uncommented in engine)
|
const verification = await cryptoEngine.verifyPayment(
|
||||||
|
tempWalletConfig.address,
|
||||||
|
transaction.amount.toString(),
|
||||||
|
selectedToken
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!verification.success) {
|
||||||
|
return NextResponse.json({
|
||||||
|
success: false,
|
||||||
|
error: "Henüz ödeme algılanmadı (On-chain bakiye yetersiz)",
|
||||||
|
status: 'waiting'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. Proceed to Sweep
|
||||||
const sweepResult = await cryptoEngine.sweepFunds(
|
const sweepResult = await cryptoEngine.sweepFunds(
|
||||||
demoTempWalletPrivKey,
|
tempWalletConfig.privateKey,
|
||||||
targetMerchant,
|
merchantAddress,
|
||||||
platformAddress,
|
platformAddress,
|
||||||
'SOL' // Using native SOL for demo
|
selectedToken
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!sweepResult.success) {
|
if (!sweepResult.success) {
|
||||||
throw new Error("Süpürme işlemi başarısız oldu.");
|
throw new Error("Süpürme işlemi başarısız oldu.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- UPDATE DATABASE STATUS ---
|
// 6. Update transaction status
|
||||||
// Marks the transaction as succeeded so it updates in the Admin dashboard
|
await db.query(`UPDATE transactions SET status = 'succeeded' WHERE id = $1`, [transaction.id]);
|
||||||
try {
|
|
||||||
await db.query(
|
// 7. Automated Webhook Notification
|
||||||
`UPDATE transactions
|
if (transaction.callback_url) {
|
||||||
SET status = 'succeeded'
|
console.log(`[Webhook] Notifying merchant at ${transaction.callback_url}`);
|
||||||
WHERE stripe_pi_id = $1`,
|
try {
|
||||||
[txId]
|
// In production, sign this payload and use a more robust delivery system
|
||||||
);
|
fetch(transaction.callback_url, {
|
||||||
} catch (dbError) {
|
method: 'POST',
|
||||||
console.error('[API] Failed to update transaction status in DB:', dbError);
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
status: 'success',
|
||||||
|
txId: transaction.stripe_pi_id,
|
||||||
|
orderRef: transaction.source_ref_id,
|
||||||
|
hashes: sweepResult
|
||||||
|
})
|
||||||
|
}).catch(e => console.error("Webhook fetch failed:", e.message));
|
||||||
|
} catch (webhookError: any) {
|
||||||
|
console.error("[Webhook Error]:", webhookError.message);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
success: true,
|
success: true,
|
||||||
message: "Ödeme başarıyla doğrulandı ve dağıtıldı (Solana Devnet).",
|
message: `Ödeme ${selectedNetwork} ağında ${selectedToken} ile başarıyla doğrulandı ve süpürüldü.`,
|
||||||
split: {
|
|
||||||
platform: "%1",
|
|
||||||
merchant: "%99"
|
|
||||||
},
|
|
||||||
hashes: {
|
hashes: {
|
||||||
platform: sweepResult.platformTx,
|
platform: sweepResult.platformTx,
|
||||||
merchant: sweepResult.merchantTx
|
merchant: sweepResult.merchantTx
|
||||||
@@ -65,6 +116,7 @@ export async function POST(request: Request) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
console.error('[API Error]:', error.message);
|
||||||
return NextResponse.json({ success: false, error: error.message }, { status: 500 });
|
return NextResponse.json({ success: false, error: error.message }, { status: 500 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
import { supabaseAdmin } from '@/lib/supabase-admin';
|
import { db } from '@/lib/db';
|
||||||
|
|
||||||
export async function GET(
|
export async function GET(
|
||||||
req: NextRequest,
|
req: NextRequest,
|
||||||
@@ -7,14 +7,11 @@ export async function GET(
|
|||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
const { id } = await context.params;
|
const { id } = await context.params;
|
||||||
const { data, error } = await supabaseAdmin
|
const result = await db.query('SELECT * FROM merchants WHERE id = $1 LIMIT 1', [id]);
|
||||||
.from('merchants')
|
const data = result.rows[0];
|
||||||
.select('*')
|
|
||||||
.eq('id', id)
|
|
||||||
.single();
|
|
||||||
|
|
||||||
if (error) {
|
if (!data) {
|
||||||
return NextResponse.json({ error: error.message }, { status: 404 });
|
return NextResponse.json({ error: 'Merchant not found' }, { status: 404 });
|
||||||
}
|
}
|
||||||
|
|
||||||
return NextResponse.json(data);
|
return NextResponse.json(data);
|
||||||
@@ -41,20 +38,14 @@ export async function PATCH(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { data, error } = await supabaseAdmin
|
const result = await db.query(
|
||||||
.from('merchants')
|
'UPDATE merchants SET name = $1, webhook_url = $2, payment_provider = $3, provider_config = $4 WHERE id = $5 RETURNING *',
|
||||||
.update({
|
[name, webhook_url, payment_provider, provider_config, id]
|
||||||
name,
|
);
|
||||||
webhook_url,
|
const data = result.rows[0];
|
||||||
payment_provider,
|
|
||||||
provider_config
|
|
||||||
})
|
|
||||||
.eq('id', id)
|
|
||||||
.select()
|
|
||||||
.single();
|
|
||||||
|
|
||||||
if (error) {
|
if (!data) {
|
||||||
return NextResponse.json({ error: error.message }, { status: 500 });
|
return NextResponse.json({ error: 'Update failed or merchant not found' }, { status: 500 });
|
||||||
}
|
}
|
||||||
|
|
||||||
return NextResponse.json(data);
|
return NextResponse.json(data);
|
||||||
@@ -72,14 +63,7 @@ export async function DELETE(
|
|||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
const { id } = await context.params;
|
const { id } = await context.params;
|
||||||
const { error } = await supabaseAdmin
|
await db.query('DELETE FROM merchants WHERE id = $1', [id]);
|
||||||
.from('merchants')
|
|
||||||
.delete()
|
|
||||||
.eq('id', id);
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
return NextResponse.json({ error: error.message }, { status: 500 });
|
|
||||||
}
|
|
||||||
|
|
||||||
return NextResponse.json({ success: true });
|
return NextResponse.json({ success: true });
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
import { supabaseAdmin } from '@/lib/supabase-admin';
|
import { db } from '@/lib/db';
|
||||||
import { cookies } from 'next/headers';
|
import { cookies } from 'next/headers';
|
||||||
|
|
||||||
export async function POST(req: NextRequest) {
|
export async function POST(req: NextRequest) {
|
||||||
@@ -12,20 +12,15 @@ export async function POST(req: NextRequest) {
|
|||||||
|
|
||||||
// 1. Resolve merchant by ID or short_id
|
// 1. Resolve merchant by ID or short_id
|
||||||
const isUUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(identifier);
|
const isUUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(identifier);
|
||||||
|
|
||||||
|
const queryText = isUUID
|
||||||
|
? 'SELECT * FROM merchants WHERE id = $1 LIMIT 1'
|
||||||
|
: 'SELECT * FROM merchants WHERE short_id = $1 LIMIT 1';
|
||||||
|
|
||||||
|
const result = await db.query(queryText, [identifier]);
|
||||||
|
const merchant = result.rows[0];
|
||||||
|
|
||||||
const query = supabaseAdmin
|
if (!merchant) {
|
||||||
.from('merchants')
|
|
||||||
.select('*');
|
|
||||||
|
|
||||||
if (isUUID) {
|
|
||||||
query.eq('id', identifier);
|
|
||||||
} else {
|
|
||||||
query.eq('short_id', identifier);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { data: merchant, error } = await query.single();
|
|
||||||
|
|
||||||
if (error || !merchant) {
|
|
||||||
return NextResponse.json({ error: 'Firma bulunamadı.' }, { status: 404 });
|
return NextResponse.json({ error: 'Firma bulunamadı.' }, { status: 404 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
import { supabaseAdmin } from '@/lib/supabase-admin';
|
import { db } from '@/lib/db';
|
||||||
|
|
||||||
export async function POST(req: NextRequest) {
|
export async function POST(req: NextRequest) {
|
||||||
try {
|
try {
|
||||||
@@ -9,20 +9,11 @@ export async function POST(req: NextRequest) {
|
|||||||
return NextResponse.json({ error: 'Mock payments are disabled' }, { status: 403 });
|
return NextResponse.json({ error: 'Mock payments are disabled' }, { status: 403 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update transaction in Supabase
|
// Update transaction in Postgres
|
||||||
const { error } = await supabaseAdmin
|
await db.query(
|
||||||
.from('transactions')
|
'UPDATE transactions SET status = $1, customer_name = $2, customer_phone = $3 WHERE stripe_pi_id = $4',
|
||||||
.update({
|
[status, customer_name, customer_phone, clientSecret]
|
||||||
status,
|
);
|
||||||
customer_name,
|
|
||||||
customer_phone
|
|
||||||
})
|
|
||||||
.eq('stripe_pi_id', clientSecret); // In mock mode, we use clientSecret as the ID
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
console.error('Mock update DB error:', error);
|
|
||||||
return NextResponse.json({ error: error.message }, { status: 500 });
|
|
||||||
}
|
|
||||||
|
|
||||||
return NextResponse.json({ success: true });
|
return NextResponse.json({ success: true });
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
|
|||||||
53
app/api/transactions/[id]/details/route.ts
Normal file
53
app/api/transactions/[id]/details/route.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
import { db } from '@/lib/db';
|
||||||
|
|
||||||
|
export async function GET(
|
||||||
|
req: NextRequest,
|
||||||
|
context: { params: Promise<{ id: string }> }
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const { id } = await context.params;
|
||||||
|
|
||||||
|
if (!id) {
|
||||||
|
return NextResponse.json({ error: 'Missing session ID' }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await db.query(`
|
||||||
|
SELECT t.*, m.name as merchant_name
|
||||||
|
FROM transactions t
|
||||||
|
JOIN merchants m ON t.merchant_id = m.id
|
||||||
|
WHERE t.id = $1
|
||||||
|
LIMIT 1
|
||||||
|
`, [id]);
|
||||||
|
|
||||||
|
if (result.rows.length === 0) {
|
||||||
|
return NextResponse.json({ error: 'Transaction not found' }, { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const tx = result.rows[0];
|
||||||
|
const metadata = tx.metadata || {};
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
id: tx.id,
|
||||||
|
amount: Number(tx.amount),
|
||||||
|
currency: tx.currency,
|
||||||
|
status: tx.status,
|
||||||
|
customer_name: tx.customer_name,
|
||||||
|
ref_id: tx.source_ref_id,
|
||||||
|
merchant_name: tx.merchant_name,
|
||||||
|
callback_url: tx.callback_url,
|
||||||
|
// Only expose public wallet addresses, not private keys
|
||||||
|
wallets: metadata.wallets ? {
|
||||||
|
EVM: metadata.wallets.EVM?.address,
|
||||||
|
SOLANA: metadata.wallets.SOLANA?.address
|
||||||
|
} : null,
|
||||||
|
clientSecret: tx.stripe_pi_id, // For Stripe/Mock
|
||||||
|
nextAction: metadata.nextAction || 'none',
|
||||||
|
redirectUrl: metadata.redirectUrl || '',
|
||||||
|
provider: tx.provider
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error: any) {
|
||||||
|
return NextResponse.json({ error: error.message }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
import { supabaseAdmin } from '@/lib/supabase-admin';
|
import { db } from '@/lib/db';
|
||||||
|
|
||||||
export async function POST(req: NextRequest) {
|
export async function POST(req: NextRequest) {
|
||||||
try {
|
try {
|
||||||
@@ -9,18 +9,10 @@ export async function POST(req: NextRequest) {
|
|||||||
return NextResponse.json({ error: 'Missing stripe_id' }, { status: 400 });
|
return NextResponse.json({ error: 'Missing stripe_id' }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const { error } = await supabaseAdmin
|
await db.query(
|
||||||
.from('transactions')
|
'UPDATE transactions SET customer_name = $1, customer_phone = $2 WHERE stripe_pi_id = $3',
|
||||||
.update({
|
[customer_name, customer_phone, stripe_id]
|
||||||
customer_name,
|
);
|
||||||
customer_phone
|
|
||||||
})
|
|
||||||
.eq('stripe_pi_id', stripe_id);
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
console.error('Update transaction info error:', error);
|
|
||||||
return NextResponse.json({ error: error.message }, { status: 500 });
|
|
||||||
}
|
|
||||||
|
|
||||||
return NextResponse.json({ success: true });
|
return NextResponse.json({ success: true });
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
|
|||||||
129
app/api/v1/checkout/route.ts
Normal file
129
app/api/v1/checkout/route.ts
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
import { db } from '@/lib/db';
|
||||||
|
import { validateApiKey } from '@/lib/api-auth';
|
||||||
|
import { CryptoEngine } from '@/lib/crypto-engine';
|
||||||
|
import { PaymentProviderFactory } from '@/lib/payment-providers';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Public API for Merchants to create a payment session
|
||||||
|
* POST /api/v1/checkout
|
||||||
|
* Header: x-api-key: YOUR_API_KEY
|
||||||
|
*/
|
||||||
|
export async function POST(req: NextRequest) {
|
||||||
|
try {
|
||||||
|
const apiKey = req.headers.get('x-api-key');
|
||||||
|
const merchant = await validateApiKey(apiKey);
|
||||||
|
|
||||||
|
if (!merchant) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Unauthorized. Invalid API Key.' },
|
||||||
|
{ status: 401 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
amount,
|
||||||
|
currency = 'TRY',
|
||||||
|
order_id,
|
||||||
|
callback_url,
|
||||||
|
customer_name,
|
||||||
|
customer_phone,
|
||||||
|
success_url,
|
||||||
|
cancel_url
|
||||||
|
} = await req.json();
|
||||||
|
|
||||||
|
if (!amount) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Amount is required.' },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Determine provider
|
||||||
|
const provider = merchant.payment_provider || 'stripe';
|
||||||
|
const useMock = process.env.NEXT_PUBLIC_USE_MOCK_PAYMENTS === 'true';
|
||||||
|
|
||||||
|
let clientSecret = '';
|
||||||
|
let providerTxId = '';
|
||||||
|
let nextAction = 'none';
|
||||||
|
let redirectUrl = '';
|
||||||
|
|
||||||
|
// Generate Temporary Wallets for Crypto fallback
|
||||||
|
const evmWallet = await CryptoEngine.createTemporaryWallet('POLYGON');
|
||||||
|
const solWallet = await CryptoEngine.createTemporaryWallet('SOLANA');
|
||||||
|
|
||||||
|
const cryptoWallets = {
|
||||||
|
EVM: { address: evmWallet.address, privateKey: evmWallet.privateKey },
|
||||||
|
SOLANA: { address: solWallet.address, privateKey: solWallet.privateKey }
|
||||||
|
};
|
||||||
|
|
||||||
|
if (useMock) {
|
||||||
|
clientSecret = 'mock_secret_' + Math.random().toString(36).substring(7);
|
||||||
|
providerTxId = clientSecret;
|
||||||
|
} else {
|
||||||
|
// Use Factory to create intent based on provider (Stripe, etc.)
|
||||||
|
const intent = await PaymentProviderFactory.createIntent(provider, {
|
||||||
|
amount,
|
||||||
|
currency,
|
||||||
|
merchantId: merchant.id,
|
||||||
|
refId: order_id,
|
||||||
|
customerName: customer_name,
|
||||||
|
customerPhone: customer_phone,
|
||||||
|
callbackUrl: callback_url || success_url,
|
||||||
|
providerConfig: merchant.provider_config
|
||||||
|
});
|
||||||
|
|
||||||
|
clientSecret = intent.clientSecret;
|
||||||
|
providerTxId = intent.providerTxId;
|
||||||
|
nextAction = intent.nextAction || 'none';
|
||||||
|
redirectUrl = intent.redirectUrl || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Insert Transaction into DB
|
||||||
|
const txResult = await db.query(`
|
||||||
|
INSERT INTO transactions (
|
||||||
|
amount, currency, status, stripe_pi_id, source_ref_id,
|
||||||
|
customer_name, customer_phone, callback_url, merchant_id,
|
||||||
|
provider, metadata
|
||||||
|
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
|
||||||
|
RETURNING id
|
||||||
|
`, [
|
||||||
|
amount, currency, 'pending', providerTxId, order_id,
|
||||||
|
customer_name, customer_phone, callback_url || success_url, merchant.id,
|
||||||
|
provider, JSON.stringify({
|
||||||
|
nextAction,
|
||||||
|
redirectUrl,
|
||||||
|
wallets: cryptoWallets,
|
||||||
|
success_url,
|
||||||
|
cancel_url
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
|
||||||
|
const txId = txResult.rows[0].id;
|
||||||
|
const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost:3000';
|
||||||
|
|
||||||
|
// 3. Return response with checkout URL
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
id: txId,
|
||||||
|
amount,
|
||||||
|
currency,
|
||||||
|
order_id,
|
||||||
|
checkout_url: `${baseUrl}/checkout?session_id=${txId}`,
|
||||||
|
status: 'pending',
|
||||||
|
wallets: {
|
||||||
|
EVM: evmWallet.address,
|
||||||
|
SOLANA: solWallet.address
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Public API Error:', error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Internal Server Error' },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
import { stripe } from '@/lib/stripe';
|
import { stripe } from '@/lib/stripe';
|
||||||
import { supabaseAdmin } from '@/lib/supabase-admin';
|
import { db } from '@/lib/db';
|
||||||
|
|
||||||
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET!;
|
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET!;
|
||||||
|
|
||||||
@@ -36,15 +36,14 @@ export async function POST(req: NextRequest) {
|
|||||||
|
|
||||||
async function handlePaymentSucceeded(paymentIntent: any) {
|
async function handlePaymentSucceeded(paymentIntent: any) {
|
||||||
// 1. Update status in our DB
|
// 1. Update status in our DB
|
||||||
const { data: transaction, error: updateError } = await supabaseAdmin
|
const result = await db.query(
|
||||||
.from('transactions')
|
'UPDATE transactions SET status = $1 WHERE stripe_pi_id = $2 RETURNING *',
|
||||||
.update({ status: 'succeeded' })
|
['succeeded', paymentIntent.id]
|
||||||
.eq('stripe_pi_id', paymentIntent.id)
|
);
|
||||||
.select('*')
|
const transaction = result.rows[0];
|
||||||
.single();
|
|
||||||
|
|
||||||
if (updateError) {
|
if (!transaction) {
|
||||||
console.error('Error updating transaction success:', updateError);
|
console.error('Transaction not found for success webhook:', paymentIntent.id);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,12 +77,8 @@ async function handlePaymentSucceeded(paymentIntent: any) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function handlePaymentFailed(paymentIntent: any) {
|
async function handlePaymentFailed(paymentIntent: any) {
|
||||||
const { error } = await supabaseAdmin
|
await db.query(
|
||||||
.from('transactions')
|
'UPDATE transactions SET status = $1 WHERE stripe_pi_id = $2',
|
||||||
.update({ status: 'failed' })
|
['failed', paymentIntent.id]
|
||||||
.eq('stripe_pi_id', paymentIntent.id);
|
);
|
||||||
|
|
||||||
if (error) {
|
|
||||||
console.error('Error updating transaction failure:', error);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,54 +12,81 @@ import Link from 'next/link';
|
|||||||
|
|
||||||
function CheckoutContent() {
|
function CheckoutContent() {
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
const amount = parseFloat(searchParams.get('amount') || '100');
|
const sessionId = searchParams.get('session_id');
|
||||||
const currency = searchParams.get('currency') || 'TL';
|
const amountParam = parseFloat(searchParams.get('amount') || '0');
|
||||||
const refId = searchParams.get('ref_id') || 'SEC-99231-TX';
|
const currencyParam = searchParams.get('currency') || 'TL';
|
||||||
const callbackUrl = searchParams.get('callback_url') || '/';
|
const refIdParam = searchParams.get('ref_id') || 'TX-DEFAULT';
|
||||||
const merchantId = searchParams.get('merchant_id') || null;
|
const callbackUrlParam = searchParams.get('callback_url') || '/';
|
||||||
|
const merchantIdParam = searchParams.get('merchant_id') || null;
|
||||||
|
|
||||||
const [clientSecret, setClientSecret] = useState<string | null>(null);
|
const [clientSecret, setClientSecret] = useState<string | null>(null);
|
||||||
const [paymentData, setPaymentData] = useState<any>(null);
|
const [paymentData, setPaymentData] = useState<any>(null);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [paymentMethod, setPaymentMethod] = useState<'card' | 'crypto'>('card');
|
const [paymentMethod, setPaymentMethod] = useState<'card' | 'crypto'>('crypto');
|
||||||
|
const [merchantName, setMerchantName] = useState<string>('Yükleniyor...');
|
||||||
|
const [displayAmount, setDisplayAmount] = useState<number>(0);
|
||||||
|
const [displayCurrency, setDisplayCurrency] = useState<string>('TRY');
|
||||||
|
|
||||||
const isMock = process.env.NEXT_PUBLIC_USE_MOCK_PAYMENTS === 'true';
|
const isMock = process.env.NEXT_PUBLIC_USE_MOCK_PAYMENTS === 'true';
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (amount <= 0) {
|
async function initializeCheckout() {
|
||||||
setError('Geçersiz işlem tutarı.');
|
try {
|
||||||
return;
|
if (sessionId) {
|
||||||
}
|
// Fetch data from existing transaction session
|
||||||
|
const res = await fetch(`/api/transactions/${sessionId}/details`);
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
|
if (data.error) {
|
||||||
|
setError(data.error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
fetch('/api/create-payment-intent', {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({
|
|
||||||
amount,
|
|
||||||
currency,
|
|
||||||
ref_id: refId,
|
|
||||||
callback_url: callbackUrl,
|
|
||||||
merchant_id: merchantId
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
.then((res) => res.json())
|
|
||||||
.then((data) => {
|
|
||||||
if (data.error) {
|
|
||||||
setError(data.error);
|
|
||||||
} else {
|
|
||||||
setClientSecret(data.clientSecret);
|
|
||||||
setPaymentData(data);
|
setPaymentData(data);
|
||||||
|
setClientSecret(data.clientSecret);
|
||||||
// Auto-redirect if it's a redirect action
|
setMerchantName(data.merchant_name);
|
||||||
|
setDisplayAmount(data.amount);
|
||||||
|
setDisplayCurrency(data.currency);
|
||||||
|
|
||||||
if (data.nextAction === 'redirect' && data.redirectUrl) {
|
if (data.nextAction === 'redirect' && data.redirectUrl) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
window.location.href = data.redirectUrl;
|
window.location.href = data.redirectUrl;
|
||||||
}, 2000); // 2 second delay to show the message
|
}, 2000);
|
||||||
}
|
}
|
||||||
|
} else if (amountParam > 0) {
|
||||||
|
// Legacy flow: create on the fly
|
||||||
|
const res = await fetch('/api/create-payment-intent', {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
amount: amountParam,
|
||||||
|
currency: currencyParam,
|
||||||
|
ref_id: refIdParam,
|
||||||
|
callback_url: callbackUrlParam,
|
||||||
|
merchant_id: merchantIdParam
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
|
if (data.error) {
|
||||||
|
setError(data.error);
|
||||||
|
} else {
|
||||||
|
setClientSecret(data.clientSecret);
|
||||||
|
setPaymentData(data);
|
||||||
|
setDisplayAmount(amountParam);
|
||||||
|
setDisplayCurrency(currencyParam);
|
||||||
|
setMerchantName('Yetkili Satıcı');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setError('Geçersiz işlem parametreleri.');
|
||||||
}
|
}
|
||||||
})
|
} catch (err) {
|
||||||
.catch(() => setError('Ödeme başlatılamadı. Lütfen tekrar deneyin.'));
|
setError('Ödeme başlatılamadı. Lütfen tekrar deneyin.');
|
||||||
}, [amount, currency, refId, callbackUrl]);
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
initializeCheckout();
|
||||||
|
}, [sessionId, amountParam, currencyParam, refIdParam, callbackUrlParam, merchantIdParam]);
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
@@ -121,7 +148,7 @@ function CheckoutContent() {
|
|||||||
<div className="pt-8 border-t border-white/10 flex flex-col sm:flex-row sm:items-center gap-8">
|
<div className="pt-8 border-t border-white/10 flex flex-col sm:flex-row sm:items-center gap-8">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-gray-400 text-[10px] font-bold uppercase tracking-wider mb-1">Satıcı</p>
|
<p className="text-gray-400 text-[10px] font-bold uppercase tracking-wider mb-1">Satıcı</p>
|
||||||
<p className="text-white font-medium text-sm">Ayris Digital Media INC.</p>
|
<p className="text-white font-medium text-sm">{merchantName}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-gray-400 text-[10px] font-bold uppercase tracking-wider mb-1">Destek</p>
|
<p className="text-gray-400 text-[10px] font-bold uppercase tracking-wider mb-1">Destek</p>
|
||||||
@@ -141,8 +168,8 @@ function CheckoutContent() {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="w-full max-w-lg">
|
<div className="w-full max-w-lg">
|
||||||
{/* Payment Method Selector */}
|
{/* Payment Method Selector (Hidden for now, only Crypto active) */}
|
||||||
<div className="flex bg-gray-100 p-1.5 rounded-2xl mb-8">
|
<div className="flex bg-gray-100 p-1.5 rounded-2xl mb-8 hidden">
|
||||||
<button
|
<button
|
||||||
onClick={() => setPaymentMethod('card')}
|
onClick={() => setPaymentMethod('card')}
|
||||||
className={`flex-1 flex items-center justify-center gap-2 py-3 rounded-xl text-[10px] font-black uppercase tracking-widest transition-all ${paymentMethod === 'card' ? 'bg-white text-gray-900 shadow-sm' : 'text-gray-400'}`}
|
className={`flex-1 flex items-center justify-center gap-2 py-3 rounded-xl text-[10px] font-black uppercase tracking-widest transition-all ${paymentMethod === 'card' ? 'bg-white text-gray-900 shadow-sm' : 'text-gray-400'}`}
|
||||||
@@ -158,13 +185,15 @@ function CheckoutContent() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{paymentMethod === 'crypto' ? (
|
{paymentMethod === 'crypto' ? (
|
||||||
<CryptoCheckout
|
<CryptoCheckout
|
||||||
amount={amount}
|
amount={displayAmount}
|
||||||
currency={currency}
|
currency={displayCurrency}
|
||||||
txId={paymentData?.clientSecret || 'TX-8231'}
|
txId={paymentData?.id || 'TX-DYNAMIC'}
|
||||||
|
wallets={paymentData?.wallets}
|
||||||
onSuccess={(hash) => {
|
onSuccess={(hash) => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
window.location.href = `${callbackUrl}?status=success&tx_hash=${hash}`;
|
const url = paymentData?.callback_url || '/';
|
||||||
|
window.location.href = `${url}${url.includes('?') ? '&' : '?'}status=success&tx_hash=${hash}`;
|
||||||
}, 2000);
|
}, 2000);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -188,13 +217,13 @@ function CheckoutContent() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
) : isMock ? (
|
) : isMock ? (
|
||||||
<MockCheckoutForm amount={amount} currency={currency} callbackUrl={callbackUrl} clientSecret={clientSecret} refId={refId} />
|
<MockCheckoutForm amount={displayAmount} currency={displayCurrency} callbackUrl={paymentData?.callback_url || '/'} clientSecret={clientSecret} refId={paymentData?.ref_id || 'REF'} />
|
||||||
) : paymentData?.provider === 'stripe' ? (
|
) : paymentData?.provider === 'stripe' ? (
|
||||||
<Elements stripe={getStripe()} options={{ clientSecret, appearance: { theme: 'stripe' } }}>
|
<Elements stripe={getStripe()} options={{ clientSecret, appearance: { theme: 'stripe' } }}>
|
||||||
<CheckoutForm
|
<CheckoutForm
|
||||||
amount={amount}
|
amount={displayAmount}
|
||||||
currency={currency}
|
currency={displayCurrency}
|
||||||
callbackUrl={callbackUrl}
|
callbackUrl={paymentData?.callback_url || '/'}
|
||||||
piId={clientSecret.split('_secret')[0]}
|
piId={clientSecret.split('_secret')[0]}
|
||||||
/>
|
/>
|
||||||
</Elements>
|
</Elements>
|
||||||
@@ -209,10 +238,10 @@ function CheckoutContent() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="mt-8 flex justify-center lg:justify-start">
|
<div className="mt-8 flex justify-center lg:justify-start">
|
||||||
<Link href={callbackUrl} className="flex items-center gap-2 text-sm font-semibold text-gray-500 hover:text-gray-900 transition translate-x-0 hover:-translate-x-1 duration-200">
|
<Link href={paymentData?.callback_url || '/'} className="flex items-center gap-2 text-sm font-semibold text-gray-500 hover:text-gray-900 transition translate-x-0 hover:-translate-x-1 duration-200">
|
||||||
<ArrowLeft size={16} />
|
<ArrowLeft size={16} />
|
||||||
Mağazaya Dön
|
Mağazaya Dön
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,34 +1,28 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { supabaseAdmin } from '@/lib/supabase-admin';
|
import { db } from '@/lib/db';
|
||||||
import {
|
import {
|
||||||
Terminal,
|
Terminal,
|
||||||
Copy,
|
Copy,
|
||||||
Check,
|
|
||||||
Globe,
|
Globe,
|
||||||
Webhook,
|
Webhook,
|
||||||
Zap,
|
Zap,
|
||||||
ShieldCheck,
|
ShieldCheck,
|
||||||
Code2
|
Code2,
|
||||||
|
Server,
|
||||||
|
Link as LinkIcon
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import Link from 'next/link';
|
|
||||||
import { cookies } from 'next/headers';
|
import { cookies } from 'next/headers';
|
||||||
import { redirect } from 'next/navigation';
|
import { redirect } from 'next/navigation';
|
||||||
|
|
||||||
async function getMerchant(identifier: string) {
|
async function getMerchant(identifier: string) {
|
||||||
const isUUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(identifier);
|
const isUUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(identifier);
|
||||||
|
|
||||||
const query = supabaseAdmin
|
const queryText = isUUID
|
||||||
.from('merchants')
|
? 'SELECT * FROM merchants WHERE id = $1 LIMIT 1'
|
||||||
.select('*');
|
: 'SELECT * FROM merchants WHERE short_id = $1 LIMIT 1';
|
||||||
|
|
||||||
if (isUUID) {
|
const result = await db.query(queryText, [identifier]);
|
||||||
query.eq('id', identifier);
|
return result.rows[0];
|
||||||
} else {
|
|
||||||
query.eq('short_id', identifier);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { data, error } = await query.single();
|
|
||||||
return data;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function MerchantIntegrationPage(props: {
|
export default async function MerchantIntegrationPage(props: {
|
||||||
@@ -45,112 +39,147 @@ export default async function MerchantIntegrationPage(props: {
|
|||||||
redirect(`/merchant/${identifier}/login`);
|
redirect(`/merchant/${identifier}/login`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const checkoutUrl = `https://p2cgateway.com/checkout?merchant_id=${merchant.short_id || merchant.id}&amount=100¤cy=TRY&ref_id=SİPARİŞ_123&callback_url=https://siteniz.com/basarili`;
|
const host = process.env.NEXT_PUBLIC_BASE_URL || 'https://p2cgateway.com';
|
||||||
|
const checkoutUrl = `${host}/checkout?merchant_id=${merchant.short_id || merchant.id}&amount=100¤cy=TRY&ref_id=SİPARİŞ_123`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-w-5xl space-y-12 animate-in fade-in slide-in-from-bottom-4 duration-700 pb-20">
|
<div className="max-w-6xl space-y-16 animate-in fade-in slide-in-from-bottom-4 duration-700 pb-32">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div>
|
<div className="flex flex-col md:flex-row md:items-end justify-between gap-6">
|
||||||
<h1 className="text-3xl font-black text-gray-900 tracking-tight">Teknik Entegrasyon</h1>
|
<div>
|
||||||
<p className="text-xs text-gray-400 font-bold uppercase tracking-widest mt-2 px-1">Ödeme sistemini sitenize nasıl bağlarsınız?</p>
|
<h1 className="text-4xl font-black text-gray-900 tracking-tight">Entegrasyon Rehberi</h1>
|
||||||
|
<p className="text-xs text-gray-400 font-black uppercase tracking-[0.3em] mt-3 px-1 border-l-4 border-blue-600 pl-4">Ödeme sistemini sisteminize dahil edin</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Quick Start Card */}
|
{/* Methods Selection */}
|
||||||
<div className="bg-gray-900 rounded-[40px] p-12 text-white relative overflow-hidden shadow-2xl">
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-10">
|
||||||
<div className="relative z-10 grid grid-cols-1 lg:grid-cols-2 gap-12 items-center">
|
{/* Option 1: Quick Link */}
|
||||||
<div className="space-y-6">
|
<div className="bg-white p-12 rounded-[48px] border border-gray-100 shadow-xl shadow-gray-100/50 space-y-10 relative overflow-hidden group">
|
||||||
<div className="flex items-center gap-4">
|
<div className="absolute top-0 right-0 w-32 h-32 bg-blue-50 rounded-full -mr-16 -mt-16 group-hover:scale-150 transition-transform duration-700"></div>
|
||||||
<div className="w-12 h-12 bg-blue-600 rounded-2xl flex items-center justify-center">
|
|
||||||
<Zap size={24} />
|
<div className="space-y-6 relative">
|
||||||
</div>
|
<div className="w-16 h-16 bg-blue-600 rounded-3xl flex items-center justify-center text-white shadow-lg shadow-blue-200">
|
||||||
<h2 className="text-2xl font-black">Hızlı Ödeme Linki</h2>
|
<LinkIcon size={32} />
|
||||||
</div>
|
</div>
|
||||||
<p className="text-gray-400 text-lg leading-relaxed font-medium">
|
<h2 className="text-2xl font-black text-gray-900">1. Hızlı Ödeme Linki</h2>
|
||||||
Entegrasyonun en basit yolu, müşterilerinizi aşağıdaki URL yapısını kullanarak ödeme sayfasına yönlendirmektir.
|
<p className="text-gray-500 font-medium leading-relaxed">
|
||||||
|
Kod yazmanıza gerek kalmadan, müşterilerinizi doğrudan ödeme sayfamıza yönlendirebilirsiniz. Parametreleri URL üzerinden iletebilirsiniz.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-white/5 p-6 rounded-3xl border border-white/10 space-y-4">
|
|
||||||
<p className="text-[10px] font-black text-gray-500 uppercase tracking-widest">Sizin Hazır Linkiniz</p>
|
<div className="space-y-4">
|
||||||
<div className="bg-black p-4 rounded-xl border border-white/5 font-mono text-[10px] text-blue-400 break-all leading-relaxed">
|
<p className="text-[10px] font-black text-gray-400 uppercase tracking-widest pl-1">Örnek Yapı</p>
|
||||||
|
<div className="bg-gray-50 p-6 rounded-3xl border border-gray-100 font-mono text-[11px] text-gray-600 break-all leading-relaxed relative group/code">
|
||||||
{checkoutUrl}
|
{checkoutUrl}
|
||||||
|
<button className="absolute top-4 right-4 p-2 bg-white rounded-lg shadow-sm opacity-0 group-hover/code:opacity-100 transition-opacity">
|
||||||
|
<Copy size={14} className="text-gray-400" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Option 2: Server-to-Server API */}
|
||||||
|
<div className="bg-gray-900 p-12 rounded-[48px] shadow-2xl space-y-10 relative overflow-hidden group">
|
||||||
|
<div className="absolute top-0 right-0 w-32 h-32 bg-white/5 rounded-full -mr-16 -mt-16"></div>
|
||||||
|
|
||||||
|
<div className="space-y-6 relative">
|
||||||
|
<div className="w-16 h-16 bg-emerald-500 rounded-3xl flex items-center justify-center text-white shadow-lg shadow-emerald-900/20">
|
||||||
|
<Server size={32} />
|
||||||
|
</div>
|
||||||
|
<h2 className="text-2xl font-black text-white">2. Profesyonel API (v1)</h2>
|
||||||
|
<p className="text-gray-400 font-medium leading-relaxed">
|
||||||
|
Sunucu taraflı entegrasyon ile daha güvenli işlemler başlatın. Fiyat manipülasyonunu engeller ve sessiz oturumlar oluşturur.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<p className="text-[10px] font-black text-gray-500 uppercase tracking-widest pl-1">Endpoint</p>
|
||||||
|
<div className="bg-white/5 p-6 rounded-3xl border border-white/10 font-mono text-[11px] text-emerald-400">
|
||||||
|
POST {host}/api/v1/checkout
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
{/* API Details Section */}
|
||||||
{/* Credentials */}
|
<div className="bg-white p-12 rounded-[48px] border border-gray-100 shadow-sm space-y-12">
|
||||||
<div className="bg-white p-10 rounded-[40px] border border-gray-100 shadow-sm space-y-8">
|
<div className="flex items-center gap-6">
|
||||||
<div className="flex items-center gap-4">
|
<div className="w-14 h-14 bg-gray-50 rounded-2xl flex items-center justify-center text-gray-900">
|
||||||
<div className="w-12 h-12 bg-blue-50 rounded-2xl flex items-center justify-center text-blue-600">
|
<ShieldCheck size={28} />
|
||||||
<ShieldCheck size={24} />
|
|
||||||
</div>
|
|
||||||
<h3 className="text-xl font-black text-gray-900">Kimlik Bilgileri</h3>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
<div className="space-y-6">
|
<h3 className="text-2xl font-black text-gray-900">API Erişimi ve Güvenlik</h3>
|
||||||
<div className="p-6 bg-gray-50 rounded-3xl border border-gray-100 space-y-3">
|
<p className="text-xs text-gray-400 font-bold uppercase tracking-widest mt-1">İsteklerinizi doğrulamak için bu anahtarları kullanın</p>
|
||||||
<label className="text-[10px] font-black text-gray-400 uppercase tracking-widest pl-1">Merchant ID (Firma Kimliği)</label>
|
|
||||||
<div className="flex items-center justify-between gap-3 bg-white p-4 rounded-xl border border-gray-200">
|
|
||||||
<code className="text-xs font-mono font-bold text-gray-600 truncate">{merchant.id}</code>
|
|
||||||
<Copy size={14} className="text-gray-300 cursor-pointer" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="p-6 bg-gray-50 rounded-3xl border border-gray-100 space-y-3">
|
|
||||||
<label className="text-[10px] font-black text-gray-400 uppercase tracking-widest pl-1">API Secret Key</label>
|
|
||||||
<div className="flex items-center justify-between gap-3 bg-white p-4 rounded-xl border border-gray-200">
|
|
||||||
<code className="text-xs font-mono font-bold text-gray-600">••••••••••••••••••••••••</code>
|
|
||||||
<button className="text-[10px] font-black text-blue-600 uppercase">Göster</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Webhooks */}
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-10">
|
||||||
<div className="bg-white p-10 rounded-[40px] border border-gray-100 shadow-sm space-y-8">
|
<div className="space-y-4">
|
||||||
<div className="flex items-center gap-4">
|
<label className="text-[10px] font-black text-gray-400 uppercase tracking-widest ml-2">Merchant ID</label>
|
||||||
<div className="w-12 h-12 bg-purple-50 rounded-2xl flex items-center justify-center text-purple-600">
|
<div className="bg-gray-50 p-5 rounded-2xl border border-gray-100 flex items-center justify-between">
|
||||||
<Webhook size={24} />
|
<code className="text-xs font-mono font-bold text-gray-600">{merchant.id}</code>
|
||||||
|
<Copy size={16} className="text-gray-300 cursor-pointer hover:text-blue-600 transition" />
|
||||||
</div>
|
</div>
|
||||||
<h3 className="text-xl font-black text-gray-900">Webhook Yapılandırması</h3>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className="text-sm text-gray-500 font-medium leading-relaxed">
|
<div className="space-y-4">
|
||||||
Ödeme başarılı olduğunda sistemimiz belirtilen adrese bir POST isteği gönderir.
|
<label className="text-[10px] font-black text-gray-400 uppercase tracking-widest ml-2">API Secret Key</label>
|
||||||
|
<div className="bg-gray-50 p-5 rounded-2xl border border-gray-100 flex items-center justify-between">
|
||||||
|
<code className="text-xs font-mono font-bold text-gray-600">
|
||||||
|
{merchant.api_key.substring(0, 8)}••••••••••••••••••••••••
|
||||||
|
</code>
|
||||||
|
<button className="text-[10px] font-black text-blue-600 uppercase tracking-widest hover:underline">Anahtarı Göster</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Webhook Settings */}
|
||||||
|
<div className="bg-white p-12 rounded-[48px] border border-gray-100 shadow-sm space-y-12">
|
||||||
|
<div className="flex items-center gap-6">
|
||||||
|
<div className="w-14 h-14 bg-purple-50 rounded-2xl flex items-center justify-center text-purple-600">
|
||||||
|
<Webhook size={28} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-2xl font-black text-gray-900">Olay Bildirimleri (Webhooks)</h3>
|
||||||
|
<p className="text-xs text-gray-400 font-bold uppercase tracking-widest mt-1">Ödeme sonuçlarını anlık olarak sunucunuzda karşılayın</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-8">
|
||||||
|
<p className="text-gray-500 font-medium leading-relaxed max-w-2xl">
|
||||||
|
İşlem tamamlandığında sistemimiz belirttiğiniz URL'ye <code className="bg-gray-100 px-2 py-1 rounded text-blue-600 font-bold">POST</code> isteği gönderir. Bu isteğin içerisinde işlemin tüm detayları yer alır.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="p-6 bg-gray-50 rounded-3xl border border-gray-100 space-y-4">
|
<div className="p-8 bg-gray-50 rounded-[32px] border border-gray-100 space-y-6">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span className="text-[10px] font-black text-gray-400 uppercase tracking-widest">Mevcut Webhook URL</span>
|
<span className="text-[11px] font-black text-gray-400 uppercase tracking-widest ml-2">Webhook URL</span>
|
||||||
<span className={`text-[10px] font-black px-2 py-0.5 rounded-md ${merchant.webhook_url ? 'bg-emerald-50 text-emerald-600' : 'bg-red-50 text-red-600'}`}>
|
<span className={`text-[10px] font-black px-3 py-1 rounded-full ${merchant.webhook_url ? 'bg-emerald-100 text-emerald-700' : 'bg-red-100 text-red-700'}`}>
|
||||||
{merchant.webhook_url ? 'AKTİF' : 'AYARLANMAMIŞ'}
|
{merchant.webhook_url ? 'HİZMETE HAZIR' : 'HENÜZ TANIMLANMAMIŞ'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-white p-4 rounded-xl border border-gray-200">
|
<div className="bg-white p-6 rounded-2xl border border-gray-200">
|
||||||
<code className="text-xs font-bold text-gray-600 truncate block">
|
<code className="text-sm font-bold text-gray-700 break-all">
|
||||||
{merchant.webhook_url || 'https://henuz-bir-adres-tanimlanmadi.com'}
|
{merchant.webhook_url || 'https://siteniz.com/api/payment-callback'}
|
||||||
</code>
|
</code>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-[10px] text-gray-400 font-bold uppercase text-center mt-2 leading-relaxed">
|
|
||||||
Webook URL adresinizi değiştirmek için <br /> destek ekibi ile iletişime geçin.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Resources */}
|
{/* Footer Resources */}
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
|
||||||
{[
|
{[
|
||||||
{ title: 'API Referansı', icon: Code2, color: 'blue' },
|
{ title: 'Postman Koleksiyonu', icon: Terminal, color: 'blue' },
|
||||||
{ title: 'Hazır Kütüphaneler', icon: Terminal, color: 'emerald' },
|
{ title: 'Geliştirici Dokümanları', icon: Code2, color: 'emerald' },
|
||||||
{ title: 'Teknik Destek', icon: Globe, color: 'purple' },
|
{ title: 'Teknik Yardım', icon: Globe, color: 'gray' },
|
||||||
].map((r) => (
|
].map((r) => (
|
||||||
<div key={r.title} className="bg-white p-8 rounded-[32px] border border-gray-100 shadow-sm flex items-center gap-6 hover:border-gray-300 transition-colors cursor-pointer group">
|
<div key={r.title} className="bg-white p-8 rounded-[32px] border border-gray-100 shadow-sm flex items-center gap-6 hover:shadow-lg transition-all cursor-pointer group hover:-translate-y-1">
|
||||||
<div className={`w-12 h-12 bg-${r.color}-50 rounded-2xl flex items-center justify-center text-${r.color}-600 group-hover:bg-${r.color}-600 group-hover:text-white transition-all`}>
|
<div className={`w-14 h-14 bg-gray-50 rounded-2xl flex items-center justify-center text-gray-400 group-hover:bg-blue-600 group-hover:text-white transition-all`}>
|
||||||
<r.icon size={22} />
|
<r.icon size={24} />
|
||||||
</div>
|
</div>
|
||||||
<span className="text-sm font-black text-gray-900 leading-tight">{r.title}</span>
|
<span className="text-sm font-black text-gray-900 uppercase tracking-tight">{r.title}</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
ShieldCheck
|
ShieldCheck
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import MerchantSidebar from '@/components/merchant/MerchantSidebar';
|
import MerchantSidebar from '@/components/merchant/MerchantSidebar';
|
||||||
import { supabaseAdmin } from '@/lib/supabase-admin';
|
import { db } from '@/lib/db';
|
||||||
|
|
||||||
export default async function MerchantLayout({
|
export default async function MerchantLayout({
|
||||||
children,
|
children,
|
||||||
@@ -26,11 +26,8 @@ export default async function MerchantLayout({
|
|||||||
let resolvedId = identifier;
|
let resolvedId = identifier;
|
||||||
|
|
||||||
if (!isUUID) {
|
if (!isUUID) {
|
||||||
const { data: merchant } = await supabaseAdmin
|
const result = await db.query('SELECT id FROM merchants WHERE short_id = $1 LIMIT 1', [identifier]);
|
||||||
.from('merchants')
|
const merchant = result.rows[0];
|
||||||
.select('id')
|
|
||||||
.eq('short_id', identifier)
|
|
||||||
.single();
|
|
||||||
if (merchant) {
|
if (merchant) {
|
||||||
resolvedId = merchant.id;
|
resolvedId = merchant.id;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { supabaseAdmin } from '@/lib/supabase-admin';
|
import { db } from '@/lib/db';
|
||||||
import {
|
import {
|
||||||
TrendingUp,
|
TrendingUp,
|
||||||
TrendingDown,
|
TrendingDown,
|
||||||
@@ -21,30 +21,23 @@ async function getMerchantData(identifier: string) {
|
|||||||
const isUUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(identifier);
|
const isUUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(identifier);
|
||||||
|
|
||||||
// Fetch merchant details
|
// Fetch merchant details
|
||||||
const query = supabaseAdmin
|
const mQueryText = isUUID
|
||||||
.from('merchants')
|
? 'SELECT * FROM merchants WHERE id = $1 LIMIT 1'
|
||||||
.select('*');
|
: 'SELECT * FROM merchants WHERE short_id = $1 LIMIT 1';
|
||||||
|
|
||||||
|
const mResult = await db.query(mQueryText, [identifier]);
|
||||||
|
const merchant = mResult.rows[0];
|
||||||
|
|
||||||
if (isUUID) {
|
if (!merchant) return null;
|
||||||
query.eq('id', identifier);
|
|
||||||
} else {
|
|
||||||
query.eq('short_id', identifier);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { data: merchant, error: mError } = await query.single();
|
|
||||||
|
|
||||||
if (mError || !merchant) return null;
|
|
||||||
|
|
||||||
const id = merchant.id; // Always use UUID for internal lookups
|
const id = merchant.id; // Always use UUID for internal lookups
|
||||||
|
|
||||||
// Fetch merchant transactions
|
// Fetch merchant transactions
|
||||||
const { data: transactions, error: tError } = await supabaseAdmin
|
const tResult = await db.query(
|
||||||
.from('transactions')
|
'SELECT * FROM transactions WHERE merchant_id = $1 ORDER BY created_at DESC',
|
||||||
.select('*')
|
[id]
|
||||||
.eq('merchant_id', id)
|
);
|
||||||
.order('created_at', { ascending: false });
|
const transactions = tResult.rows;
|
||||||
|
|
||||||
if (tError) return null;
|
|
||||||
|
|
||||||
const successfulTransactions = transactions.filter(t => t.status === 'succeeded');
|
const successfulTransactions = transactions.filter(t => t.status === 'succeeded');
|
||||||
const totalRevenue = successfulTransactions.reduce((acc, t) => acc + Number(t.amount), 0);
|
const totalRevenue = successfulTransactions.reduce((acc, t) => acc + Number(t.amount), 0);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { supabaseAdmin } from '@/lib/supabase-admin';
|
import { db } from '@/lib/db';
|
||||||
import { format } from 'date-fns';
|
import { format } from 'date-fns';
|
||||||
import { tr } from 'date-fns/locale';
|
import { tr } from 'date-fns/locale';
|
||||||
import {
|
import {
|
||||||
@@ -16,27 +16,20 @@ import { redirect } from 'next/navigation';
|
|||||||
async function getMerchantTransactions(identifier: string) {
|
async function getMerchantTransactions(identifier: string) {
|
||||||
const isUUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(identifier);
|
const isUUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(identifier);
|
||||||
|
|
||||||
const query = supabaseAdmin
|
const mQueryText = isUUID
|
||||||
.from('merchants')
|
? 'SELECT id FROM merchants WHERE id = $1 LIMIT 1'
|
||||||
.select('id')
|
: 'SELECT id FROM merchants WHERE short_id = $1 LIMIT 1';
|
||||||
|
|
||||||
if (isUUID) {
|
const mResult = await db.query(mQueryText, [identifier]);
|
||||||
query.eq('id', identifier);
|
const merchant = mResult.rows[0];
|
||||||
} else {
|
|
||||||
query.eq('short_id', identifier);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { data: merchant } = await query.single();
|
|
||||||
if (!merchant) return null;
|
if (!merchant) return null;
|
||||||
|
|
||||||
const { data, error } = await supabaseAdmin
|
const tResult = await db.query(
|
||||||
.from('transactions')
|
'SELECT * FROM transactions WHERE merchant_id = $1 ORDER BY created_at DESC',
|
||||||
.select('*')
|
[merchant.id]
|
||||||
.eq('merchant_id', merchant.id)
|
);
|
||||||
.order('created_at', { ascending: false });
|
|
||||||
|
|
||||||
if (error) return null;
|
return tResult.rows;
|
||||||
return data;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function MerchantTransactionsPage(props: {
|
export default async function MerchantTransactionsPage(props: {
|
||||||
@@ -53,7 +46,8 @@ export default async function MerchantTransactionsPage(props: {
|
|||||||
const isUUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(identifier);
|
const isUUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(identifier);
|
||||||
let resolvedId = identifier;
|
let resolvedId = identifier;
|
||||||
if (!isUUID) {
|
if (!isUUID) {
|
||||||
const { data: merchant } = await supabaseAdmin.from('merchants').select('id').eq('short_id', identifier).single();
|
const result = await db.query('SELECT id FROM merchants WHERE short_id = $1 LIMIT 1', [identifier]);
|
||||||
|
const merchant = result.rows[0];
|
||||||
if (merchant) resolvedId = merchant.id;
|
if (merchant) resolvedId = merchant.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,55 +2,110 @@
|
|||||||
|
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import {
|
import {
|
||||||
Coins,
|
|
||||||
Copy,
|
Copy,
|
||||||
CheckCircle2,
|
CheckCircle2,
|
||||||
ExternalLink,
|
|
||||||
RefreshCw,
|
RefreshCw,
|
||||||
AlertCircle,
|
AlertCircle,
|
||||||
QrCode
|
ChevronDown
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
import cryptoConfig from '@/lib/crypto-config.json';
|
||||||
|
|
||||||
interface CryptoCheckoutProps {
|
interface CryptoCheckoutProps {
|
||||||
amount: number;
|
amount: number;
|
||||||
currency: string;
|
currency: string;
|
||||||
txId: string;
|
txId: string;
|
||||||
|
wallets?: {
|
||||||
|
EVM: string;
|
||||||
|
SOLANA: string;
|
||||||
|
};
|
||||||
onSuccess: (txHash: string) => void;
|
onSuccess: (txHash: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function CryptoCheckout({ amount, currency, txId, onSuccess }: CryptoCheckoutProps) {
|
export default function CryptoCheckout({ amount, currency, txId, wallets, onSuccess }: CryptoCheckoutProps) {
|
||||||
const [selectedCoin, setSelectedCoin] = useState('SOL');
|
const [selectedNetwork, setSelectedNetwork] = useState(cryptoConfig.networks[0]);
|
||||||
const [depositAddress, setDepositAddress] = useState<string>('');
|
const [selectedToken, setSelectedToken] = useState(selectedNetwork.tokens[0]);
|
||||||
|
const [depositAddress, setDepositAddress] = useState<string>('Yükleniyor...');
|
||||||
const [isVerifying, setIsVerifying] = useState(false);
|
const [isVerifying, setIsVerifying] = useState(false);
|
||||||
const [copied, setCopied] = useState(false);
|
const [copied, setCopied] = useState(false);
|
||||||
const [status, setStatus] = useState<'waiting' | 'verifying' | 'success' | 'error'>('waiting');
|
const [status, setStatus] = useState<'waiting' | 'verifying' | 'success' | 'error'>('waiting');
|
||||||
const [cryptoAmount, setCryptoAmount] = useState<string>('Hesaplanıyor...');
|
const [cryptoAmount, setCryptoAmount] = useState<string>('Hesaplanıyor...');
|
||||||
|
const [unitPrice, setUnitPrice] = useState<string>('');
|
||||||
|
const [unitPriceUsd, setUnitPriceUsd] = useState<string>('');
|
||||||
|
|
||||||
|
// Update address based on selected network
|
||||||
|
useEffect(() => {
|
||||||
|
if (wallets) {
|
||||||
|
const addr = selectedNetwork.id === 'SOLANA' ? wallets.SOLANA : wallets.EVM;
|
||||||
|
setDepositAddress(addr);
|
||||||
|
}
|
||||||
|
}, [selectedNetwork, wallets]);
|
||||||
|
|
||||||
|
// Fetch exchange rate
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function fetchExchangeRate() {
|
async function fetchExchangeRate() {
|
||||||
if (currency === 'TL' || currency === 'TRY') {
|
setCryptoAmount('...');
|
||||||
try {
|
try {
|
||||||
const symbol = selectedCoin === 'SOL' ? 'SOLTRY' : 'USDTTRY';
|
const symbol = selectedToken.symbol;
|
||||||
const res = await fetch(`https://api.binance.com/api/v3/ticker/price?symbol=${symbol}`);
|
let rateInTry = 32.5;
|
||||||
const data = await res.json();
|
let rateInUsd = 1.0;
|
||||||
const rate = parseFloat(data.price);
|
|
||||||
setCryptoAmount((amount / rate).toFixed(selectedCoin === 'SOL' ? 4 : 2));
|
// 1. Get USDTRY rate first
|
||||||
} catch (error) {
|
const tryRes = await fetch(`https://api.binance.com/api/v3/ticker/price?symbol=USDTTRY`);
|
||||||
// Fallback rate if API fails
|
const tryData = await tryRes.json();
|
||||||
setCryptoAmount((amount / 5500).toFixed(selectedCoin === 'SOL' ? 4 : 2));
|
const tryToUsdRate = parseFloat(tryData.price) || 32.5;
|
||||||
|
|
||||||
|
const isStable = ['USDT', 'USDC', 'BUSD', 'DAI'].includes(symbol);
|
||||||
|
|
||||||
|
if (!isStable) {
|
||||||
|
// Try to fetch [SYMBOL]USDT
|
||||||
|
try {
|
||||||
|
const pair = `${symbol}USDT`;
|
||||||
|
const res = await fetch(`https://api.binance.com/api/v3/ticker/price?symbol=${pair}`);
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
|
if (data.price) {
|
||||||
|
rateInUsd = parseFloat(data.price);
|
||||||
|
rateInTry = rateInUsd * tryToUsdRate;
|
||||||
|
} else {
|
||||||
|
// Try fallback mappings if direct USDT pair fails
|
||||||
|
rateInTry = tryToUsdRate; // Default to 1 USD value
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
rateInTry = tryToUsdRate;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
rateInTry = tryToUsdRate;
|
||||||
|
rateInUsd = 1.0;
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
// If already USD or USDT, 1:1 ratio
|
setUnitPrice(rateInTry.toLocaleString('tr-TR', { minimumFractionDigits: 2, maximumFractionDigits: 4 }));
|
||||||
setCryptoAmount(amount.toFixed(2));
|
setUnitPriceUsd(rateInUsd.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 4 }));
|
||||||
|
|
||||||
|
const finalAmount = (amount / rateInTry).toFixed(selectedToken.decimals < 9 ? 4 : 6);
|
||||||
|
setCryptoAmount(finalAmount);
|
||||||
|
} catch (error) {
|
||||||
|
setCryptoAmount((amount / 32.5).toFixed(2));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fetchExchangeRate();
|
fetchExchangeRate();
|
||||||
}, [amount, currency]);
|
}, [amount, selectedToken, selectedNetwork]);
|
||||||
|
|
||||||
|
// Auto-polling for payment verification
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Use a real valid Solana test wallet so Phantom doesn't say "Invalid Address"
|
let interval: NodeJS.Timeout;
|
||||||
setDepositAddress('5pLH1tqZhx8p8WpZ18yr28N42KXB3FXVPzZ9ceCtpBVe');
|
if (status === 'waiting' && depositAddress !== 'Yükleniyor...') {
|
||||||
}, [selectedCoin]);
|
interval = setInterval(() => {
|
||||||
|
verifyPayment();
|
||||||
|
}, 8000); // Check every 8 seconds
|
||||||
|
}
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, [status, depositAddress, selectedNetwork, selectedToken]);
|
||||||
|
|
||||||
|
const changeNetwork = (networkId: string) => {
|
||||||
|
const network = cryptoConfig.networks.find(n => n.id === networkId) || cryptoConfig.networks[0];
|
||||||
|
setSelectedNetwork(network);
|
||||||
|
setSelectedToken(network.tokens[0]);
|
||||||
|
};
|
||||||
|
|
||||||
const handleCopy = () => {
|
const handleCopy = () => {
|
||||||
navigator.clipboard.writeText(depositAddress);
|
navigator.clipboard.writeText(depositAddress);
|
||||||
@@ -60,118 +115,136 @@ export default function CryptoCheckout({ amount, currency, txId, onSuccess }: Cr
|
|||||||
|
|
||||||
const verifyPayment = async () => {
|
const verifyPayment = async () => {
|
||||||
setIsVerifying(true);
|
setIsVerifying(true);
|
||||||
setStatus('verifying');
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/crypto-sweep', {
|
const response = await fetch('/api/crypto-sweep', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
txId: txId,
|
txId: txId,
|
||||||
merchantAddress: '5pLH1tqZhx8p8WpZ18yr28N42KXB3FXVPzZ9ceCtpBVe' // A placeholder valid Solana Devnet Wallet
|
network: selectedNetwork.id,
|
||||||
|
token: selectedToken.symbol
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
setStatus('success');
|
setStatus('success');
|
||||||
onSuccess(data.hashes.merchant);
|
onSuccess(data.hashes?.merchant || '0x_mock_hash');
|
||||||
} else {
|
} else if (data.status === 'waiting') {
|
||||||
setStatus('error');
|
setStatus('waiting');
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setStatus('error');
|
console.error(err);
|
||||||
} finally {
|
} finally {
|
||||||
setIsVerifying(false);
|
setIsVerifying(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const qrUrl = `https://api.qrserver.com/v1/create-qr-code/?size=250x250&data=${depositAddress}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-white p-8 lg:p-12 rounded-[40px] border border-gray-100 shadow-sm space-y-8 animate-in fade-in zoom-in duration-500 w-full max-w-lg">
|
<div className="bg-white p-8 rounded-[40px] border border-gray-100 shadow-sm space-y-6 w-full max-w-lg">
|
||||||
<div className="flex items-center justify-between">
|
{/* Crypto Selection Header */}
|
||||||
|
<div className="grid grid-cols-2 gap-3">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-[10px] font-black text-gray-400 uppercase tracking-widest ml-1">Ağ Seçin</label>
|
||||||
|
<div className="relative group">
|
||||||
|
<select
|
||||||
|
value={selectedNetwork.id}
|
||||||
|
onChange={(e) => changeNetwork(e.target.value)}
|
||||||
|
className="w-full bg-gray-50 border-none rounded-2xl p-4 text-sm font-bold text-gray-900 appearance-none cursor-pointer focus:ring-2 focus:ring-blue-500/20"
|
||||||
|
>
|
||||||
|
{cryptoConfig.networks.map(net => (
|
||||||
|
<option key={net.id} value={net.id}>{net.icon} {net.name}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<ChevronDown className="absolute right-4 top-1/2 -translate-y-1/2 text-gray-400 pointer-events-none" size={16} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-[10px] font-black text-gray-400 uppercase tracking-widest ml-1">Varlık</label>
|
||||||
|
<div className="relative">
|
||||||
|
<select
|
||||||
|
value={selectedToken.symbol}
|
||||||
|
onChange={(e) => setSelectedToken(selectedNetwork.tokens.find(t => t.symbol === e.target.value) || selectedNetwork.tokens[0])}
|
||||||
|
className="w-full bg-gray-50 border-none rounded-2xl p-4 text-sm font-bold text-gray-900 appearance-none cursor-pointer focus:ring-2 focus:ring-blue-500/20"
|
||||||
|
>
|
||||||
|
{selectedNetwork.tokens.map(token => (
|
||||||
|
<option key={token.symbol} value={token.symbol}>{token.symbol}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<ChevronDown className="absolute right-4 top-1/2 -translate-y-1/2 text-gray-400 pointer-events-none" size={16} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-6 bg-gray-50 rounded-3xl flex items-center justify-between border border-gray-100">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="p-3 bg-orange-50 rounded-2xl text-orange-600">
|
<div className="w-12 h-12 bg-white rounded-2xl flex items-center justify-center text-2xl shadow-sm">
|
||||||
<Coins size={24} />
|
{selectedToken.symbol === 'SOL' ? '☀️' : selectedToken.symbol.startsWith('U') ? '💵' : '🪙'}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-xl font-black text-gray-900 uppercase tracking-tight">Kripto Ödeme</h3>
|
<p className="text-[10px] text-gray-400 font-bold uppercase tracking-tight">Ödenecek Tutar</p>
|
||||||
<p className="text-[10px] text-gray-400 font-black uppercase tracking-widest">On-Chain Güvenli Transfer</p>
|
<h4 className="text-2xl font-black text-gray-900 leading-none">{cryptoAmount} {selectedToken.symbol}</h4>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-right">
|
<div className="text-right">
|
||||||
<p className="text-2xl font-black text-gray-900">{cryptoAmount} <span className="text-xs text-gray-400">{selectedCoin}</span></p>
|
<p className="text-[10px] text-gray-400 font-bold uppercase">Kur</p>
|
||||||
<p className="text-[10px] text-gray-400 font-bold uppercase">Ağ: Solana (Devnet)</p>
|
<p className="text-[10px] font-black text-blue-600 mb-1">1 {selectedToken.symbol} = {unitPrice} TRY</p>
|
||||||
|
<p className="text-[10px] font-bold text-gray-400">($ {unitPriceUsd})</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{status === 'success' ? (
|
{status === 'success' ? (
|
||||||
<div className="py-10 text-center space-y-6">
|
<div className="py-8 text-center space-y-4 bg-emerald-50 rounded-3xl border border-emerald-100 animate-in fade-in slide-in-from-bottom-4">
|
||||||
<div className="w-20 h-20 bg-emerald-50 rounded-[32px] flex items-center justify-center text-emerald-500 mx-auto animate-bounce">
|
<div className="w-16 h-16 bg-emerald-500 rounded-full flex items-center justify-center text-white mx-auto">
|
||||||
<CheckCircle2 size={40} />
|
<CheckCircle2 size={32} />
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<h2 className="text-2xl font-black text-gray-900 uppercase tracking-tight">Ödeme Onaylandı!</h2>
|
|
||||||
<p className="text-gray-400 font-bold uppercase tracking-widest text-[10px]">İşleminiz başarıyla blokzincirine kaydedildi.</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
<h2 className="text-xl font-black text-emerald-900 uppercase">Ödeme Alındı</h2>
|
||||||
|
<p className="text-emerald-700 font-bold text-[10px] uppercase tracking-widest">Mağazaya yönlendiriliyorsunuz...</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{/* QR Code Placeholder */}
|
|
||||||
<div className="bg-gray-50 aspect-square rounded-[32px] flex flex-col items-center justify-center border border-gray-100 relative group cursor-pointer">
|
|
||||||
<QrCode size={180} className="text-gray-200 group-hover:text-gray-400 transition-colors" />
|
|
||||||
<div className="absolute inset-0 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity">
|
|
||||||
<span className="bg-white px-4 py-2 rounded-xl text-[10px] font-black uppercase shadow-lg border border-gray-100">Büyüt</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
|
<div className="bg-white aspect-square rounded-[32px] flex flex-col items-center justify-center border-2 border-dashed border-gray-100 relative group overflow-hidden p-8">
|
||||||
|
{depositAddress !== 'Yükleniyor...' ? (
|
||||||
|
<img src={qrUrl} alt="Wallet QR" className="w-full h-full object-contain" />
|
||||||
|
) : (
|
||||||
|
<RefreshCw className="w-12 h-12 text-gray-200 animate-spin" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="text-[10px] font-black text-gray-400 uppercase tracking-widest pl-1">Cüzdan Adresi</label>
|
<label className="text-[10px] font-black text-gray-400 uppercase tracking-widest ml-1">Cüzdan Adresi ({selectedNetwork.name})</label>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="flex-1 bg-gray-50 p-4 rounded-2xl border border-gray-100 font-mono text-[10px] text-gray-600 break-all leading-tight">
|
<div className="flex-1 bg-gray-50 p-4 rounded-2xl border border-gray-100 font-mono text-[10px] text-gray-500 break-all leading-tight">
|
||||||
{depositAddress}
|
{depositAddress}
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={handleCopy}
|
onClick={handleCopy}
|
||||||
className="p-4 bg-gray-900 text-white rounded-2xl hover:bg-black transition-all shadow-lg shadow-gray-200 active:scale-95"
|
className="p-4 bg-gray-900 text-white rounded-2xl hover:bg-black transition-all shadow-lg active:scale-95"
|
||||||
>
|
>
|
||||||
{copied ? <CheckCircle2 size={18} /> : <Copy size={18} />}
|
{copied ? <CheckCircle2 size={18} /> : <span>Kopyala</span>}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="p-6 bg-blue-50/50 rounded-3xl border border-blue-100 space-y-2">
|
<div className="p-4 bg-orange-50 rounded-2xl border border-orange-100 flex gap-3">
|
||||||
<div className="flex items-center gap-2 text-blue-600">
|
<AlertCircle size={18} className="text-orange-600 shrink-0" />
|
||||||
<AlertCircle size={14} />
|
<p className="text-[10px] text-orange-800 leading-relaxed font-bold">
|
||||||
<span className="text-[9px] font-black uppercase tracking-widest">Önemli Uyarı</span>
|
Sadece <span className="underline text-orange-950">{selectedNetwork.name}</span> ağı üzerinden <span className="underline text-orange-950">{selectedToken.symbol}</span> gönderin.
|
||||||
</div>
|
|
||||||
<p className="text-[10px] text-blue-800 leading-relaxed font-medium">
|
|
||||||
Lütfen sadece test amaçlı <b>Solana (Devnet)</b> ağı üzerinden test SOL'ü gönderimi yapın. Gerçek ağ veya USDT bu ortamda kabul edilmez.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={verifyPayment}
|
onClick={verifyPayment}
|
||||||
disabled={isVerifying}
|
disabled={isVerifying || depositAddress === 'Yükleniyor...'}
|
||||||
className="w-full py-5 bg-gray-900 text-white rounded-2xl font-black text-xs uppercase tracking-[0.2em] hover:bg-black transition-all shadow-xl shadow-gray-200 flex items-center justify-center gap-3 disabled:opacity-50"
|
className="w-full py-5 bg-gray-900 text-white rounded-2xl font-black text-xs uppercase tracking-[0.2em] transition-all shadow-xl flex items-center justify-center gap-3 disabled:opacity-50"
|
||||||
>
|
>
|
||||||
{isVerifying ? (
|
<span className="w-2 h-2 bg-blue-500 rounded-full animate-pulse" />
|
||||||
<>
|
{isVerifying ? 'Doğrulanıyor...' : 'Otomatik Tarama Aktif'}
|
||||||
<RefreshCw size={18} className="animate-spin" />
|
|
||||||
Doğrulanıyor...
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
'Ödemeyi Doğrula'
|
|
||||||
)}
|
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div className="flex justify-center gap-6">
|
|
||||||
<div className="flex items-center gap-2 text-gray-400 hover:text-gray-600 cursor-pointer transition-colors group">
|
|
||||||
<ExternalLink size={14} />
|
|
||||||
<span className="text-[9px] font-black uppercase tracking-widest group-hover:underline">Explorer'da Gör</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
21
lib/api-auth.ts
Normal file
21
lib/api-auth.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { db } from './db';
|
||||||
|
|
||||||
|
export async function validateApiKey(apiKey: string | null) {
|
||||||
|
if (!apiKey) return null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await db.query(
|
||||||
|
'SELECT * FROM merchants WHERE api_key = $1 LIMIT 1',
|
||||||
|
[apiKey]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.rows.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.rows[0];
|
||||||
|
} catch (error) {
|
||||||
|
console.error('API Key Validation Error:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
67
lib/crypto-config.json
Normal file
67
lib/crypto-config.json
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
{
|
||||||
|
"networks": [
|
||||||
|
{
|
||||||
|
"id": "POLYGON",
|
||||||
|
"name": "Polygon",
|
||||||
|
"icon": "🟣",
|
||||||
|
"rpc": "https://rpc.ankr.com/polygon",
|
||||||
|
"tokens": [
|
||||||
|
{ "symbol": "USDT", "address": "0xc2132D05D31C914a87C6611C10748AEb04B58e8F", "decimals": 6 },
|
||||||
|
{ "symbol": "USDC", "address": "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359", "decimals": 6 },
|
||||||
|
{ "symbol": "DAI", "address": "0x8f3Cf7ad23Cd3BaDDb9735AFf95930030000000", "decimals": 18 },
|
||||||
|
{ "symbol": "MATIC", "address": "NATIVE", "decimals": 18 },
|
||||||
|
{ "symbol": "WBTC", "address": "0x1bfd67037b42cf73acf2047067bd4f2c47d9bfd6", "decimals": 8 },
|
||||||
|
{ "symbol": "WETH", "address": "0x7ceb23fd6bc0ad59e62ac25578270cff1b9f619", "decimals": 18 },
|
||||||
|
{ "symbol": "SHIB", "address": "0x6f8a36397efed74758fdef2850935bb27d49e1ed", "decimals": 18 },
|
||||||
|
{ "symbol": "LINK", "address": "0xb0897686c545045aFc77CF20eC7A532E3120E0F1", "decimals": 18 },
|
||||||
|
{ "symbol": "PEPE", "address": "0x98f6d546343544fae8e60aaead11a68e64c29df6", "decimals": 18 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "BSC",
|
||||||
|
"name": "BNB Chain",
|
||||||
|
"icon": "🟡",
|
||||||
|
"rpc": "https://rpc.ankr.com/bsc",
|
||||||
|
"tokens": [
|
||||||
|
{ "symbol": "USDT", "address": "0x55d398326f99059fF775485246999027B3197955", "decimals": 18 },
|
||||||
|
{ "symbol": "USDC", "address": "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d", "decimals": 18 },
|
||||||
|
{ "symbol": "BNB", "address": "NATIVE", "decimals": 18 },
|
||||||
|
{ "symbol": "BTCCB", "address": "0x7130d2a12b9bcbfae4f2634d864a1ee1ce3ead9c", "decimals": 18 },
|
||||||
|
{ "symbol": "ETH", "address": "0x2170ed0880ac9a755fd29b2688956bd959f933f8", "decimals": 18 },
|
||||||
|
{ "symbol": "XRP", "address": "0x1d2f0da169059048e02d847144ee6dd583849764", "decimals": 18 },
|
||||||
|
{ "symbol": "ADA", "address": "0x3ee2200efb3400fabb9aacf31297cbdd1d435d47", "decimals": 18 },
|
||||||
|
{ "symbol": "DOGE", "address": "0xba2ae424d960c26247dd5c32ed17016355e8eb10", "decimals": 8 },
|
||||||
|
{ "symbol": "DOT", "address": "0x7083609fce4d1d8dc0c979aab8c869ea2c873402", "decimals": 18 },
|
||||||
|
{ "symbol": "LTC", "address": "0x4338665c00995c36411f1233069cc04868f18731", "decimals": 18 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "ETH",
|
||||||
|
"name": "Ethereum",
|
||||||
|
"icon": "🔵",
|
||||||
|
"rpc": "https://rpc.ankr.com/eth",
|
||||||
|
"tokens": [
|
||||||
|
{ "symbol": "USDT", "address": "0xdAC17F958D2ee523a2206206994597C13D831ec7", "decimals": 6 },
|
||||||
|
{ "symbol": "USDC", "address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", "decimals": 6 },
|
||||||
|
{ "symbol": "DAI", "address": "0x6B175474E89094C44Da98b954EedeAC495271d0F", "decimals": 18 },
|
||||||
|
{ "symbol": "ETH", "address": "NATIVE", "decimals": 18 },
|
||||||
|
{ "symbol": "WBTC", "address": "0x2260fac5e5542a773aa44fbcfedf7c193bc2c599", "decimals": 8 },
|
||||||
|
{ "symbol": "SHIB", "address": "0x95ad61b0a150d79219dcf64e1e6cc01f0b64c4ce", "decimals": 18 },
|
||||||
|
{ "symbol": "LINK", "address": "0x514910771af9ca656af840dff83e8264ecf986ca", "decimals": 18 },
|
||||||
|
{ "symbol": "UNI", "address": "0x1f9840a85d5af5bf1d1762f925bdaddc4201f984", "decimals": 18 },
|
||||||
|
{ "symbol": "PEPE", "address": "0x6982508145454ce325ddbe47a25d4ec3d2311933", "decimals": 18 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "SOLANA",
|
||||||
|
"name": "Solana (Dev)",
|
||||||
|
"icon": "🟢",
|
||||||
|
"rpc": "https://api.devnet.solana.com",
|
||||||
|
"tokens": [
|
||||||
|
{ "symbol": "SOL", "address": "NATIVE", "decimals": 9 },
|
||||||
|
{ "symbol": "USDC", "address": "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU", "decimals": 6 },
|
||||||
|
{ "symbol": "USDT", "address": "EJwZgeZrdC8TXTQbQBoL6bfuAnFUUy1PVCMB4DYPzVaS", "decimals": 6 }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,57 +1,43 @@
|
|||||||
import { ethers } from 'ethers';
|
import { ethers } from 'ethers';
|
||||||
import { Connection, PublicKey, Keypair, Transaction, SystemProgram, sendAndConfirmTransaction, clusterApiUrl, LAMPORTS_PER_SOL } from '@solana/web3.js';
|
import { Connection, PublicKey, Keypair, Transaction, SystemProgram, clusterApiUrl, LAMPORTS_PER_SOL } from '@solana/web3.js';
|
||||||
import { getAssociatedTokenAddress, getAccount, createTransferInstruction } from '@solana/spl-token';
|
import { getAssociatedTokenAddress, getAccount } from '@solana/spl-token';
|
||||||
import bs58 from 'bs58';
|
import bs58 from 'bs58';
|
||||||
|
import cryptoConfig from './crypto-config.json';
|
||||||
// Demo configuration - In production, these should be securely managed
|
|
||||||
const RPC_URLS: Record<string, string> = {
|
|
||||||
ETH: 'https://rpc.ankr.com/eth',
|
|
||||||
POLYGON: 'https://rpc.ankr.com/polygon',
|
|
||||||
BSC: 'https://rpc.ankr.com/bsc'
|
|
||||||
};
|
|
||||||
|
|
||||||
// AyrisSplitter Contract Address (Example addresses, in production use real deployed addresses)
|
|
||||||
const SPLITTER_ADDRESSES: Record<string, string> = {
|
|
||||||
POLYGON: '0x999...AYRIS_SPLITTER_POLYGON',
|
|
||||||
ETH: '0x888...AYRIS_SPLITTER_ETH'
|
|
||||||
};
|
|
||||||
|
|
||||||
// ERC20 ABI for checking USDT/USDC balances
|
// ERC20 ABI for checking USDT/USDC balances
|
||||||
const ERC20_ABI = [
|
const ERC20_ABI = [
|
||||||
"function balanceOf(address owner) view returns (uint256)",
|
"function balanceOf(address owner) view returns (uint256)",
|
||||||
"function decimals() view returns (uint8)",
|
"function decimals() view returns (uint8)",
|
||||||
"function symbol() view returns (string)"
|
"function symbol() view returns (string)",
|
||||||
|
"function transfer(address to, uint256 value) public returns (bool)"
|
||||||
];
|
];
|
||||||
|
|
||||||
const STABLECOIN_ADDRESSES: Record<string, Record<string, string>> = {
|
|
||||||
POLYGON: {
|
|
||||||
USDT: '0xc2132D05D31c914a87C6611C10748AEb04B58e8F',
|
|
||||||
USDC: '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359'
|
|
||||||
},
|
|
||||||
ETH: {
|
|
||||||
USDT: '0xdAC17F958D2ee523a2206206994597C13D831ec7',
|
|
||||||
USDC: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48'
|
|
||||||
},
|
|
||||||
SOLANA: {
|
|
||||||
USDC: '4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU', // Devnet USDC
|
|
||||||
USDT: 'EJwZgeZrdC8TXTQbQBoL6bfuAnFUUy1PVCMB4DYPzVaS' // Devnet USDT
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export class CryptoEngine {
|
export class CryptoEngine {
|
||||||
private provider!: ethers.JsonRpcProvider;
|
private provider!: ethers.JsonRpcProvider;
|
||||||
private solConnection!: Connection;
|
private solConnection!: Connection;
|
||||||
private network: string;
|
private network: string;
|
||||||
|
private config: any;
|
||||||
|
|
||||||
constructor(network: string = 'POLYGON') {
|
constructor(networkId: string = 'POLYGON') {
|
||||||
this.network = network;
|
this.network = networkId;
|
||||||
if (network === 'SOLANA') {
|
this.config = cryptoConfig.networks.find(n => n.id === networkId);
|
||||||
this.solConnection = new Connection(clusterApiUrl('devnet'), 'confirmed');
|
|
||||||
|
if (!this.config) throw new Error(`Network ${networkId} not found in config.`);
|
||||||
|
|
||||||
|
if (this.network === 'SOLANA') {
|
||||||
|
this.solConnection = new Connection(this.config.rpc, 'confirmed');
|
||||||
} else {
|
} else {
|
||||||
this.provider = new ethers.JsonRpcProvider(RPC_URLS[network]);
|
this.provider = new ethers.JsonRpcProvider(this.config.rpc);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper to get token config from JSON
|
||||||
|
*/
|
||||||
|
private getTokenConfig(symbol: string) {
|
||||||
|
return this.config.tokens.find((t: any) => t.symbol === symbol);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates a temporary wallet for a transaction.
|
* Generates a temporary wallet for a transaction.
|
||||||
*/
|
*/
|
||||||
@@ -94,19 +80,12 @@ export class CryptoEngine {
|
|||||||
tokenSymbol: string
|
tokenSymbol: string
|
||||||
) {
|
) {
|
||||||
const tempWallet = new ethers.Wallet(tempWalletPrivateKey, this.provider);
|
const tempWallet = new ethers.Wallet(tempWalletPrivateKey, this.provider);
|
||||||
const tokenAddress = STABLECOIN_ADDRESSES[this.network][tokenSymbol];
|
const tokenConfig = this.getTokenConfig(tokenSymbol);
|
||||||
const contract = new ethers.Contract(tokenAddress, ERC20_ABI, tempWallet);
|
if (!tokenConfig) throw new Error(`Unsupported token ${tokenSymbol} on ${this.network}`);
|
||||||
|
|
||||||
const balance = await contract.balanceOf(tempWallet.address);
|
console.log(`[Sweep EVM] Network: ${this.network} Total for ${tokenSymbol}`);
|
||||||
if (balance === 0n) throw new Error("Balance is zero");
|
|
||||||
|
|
||||||
const platformShare = (balance * 100n) / 10000n; // %1
|
|
||||||
const merchantShare = balance - platformShare; // %99
|
|
||||||
|
|
||||||
console.log(`[Sweep EVM] Total: ${ethers.formatUnits(balance, 6)} ${tokenSymbol}`);
|
|
||||||
console.log(`[Sweep EVM] Sending ${ethers.formatUnits(platformShare, 6)} to Platform...`);
|
|
||||||
console.log(`[Sweep EVM] Sending ${ethers.formatUnits(merchantShare, 6)} to Merchant...`);
|
|
||||||
|
|
||||||
|
// Mocking the real transfer for demo
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
platformTx: '0x' + Math.random().toString(16).slice(2, 66),
|
platformTx: '0x' + Math.random().toString(16).slice(2, 66),
|
||||||
@@ -114,6 +93,26 @@ export class CryptoEngine {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fuelWallet(targetAddress: string, amount: string) {
|
||||||
|
const gasTankKey = process.env.CRYPTO_GAS_TANK_KEY;
|
||||||
|
if (!gasTankKey) {
|
||||||
|
console.warn("[CryptoEngine] No CRYPTO_GAS_TANK_KEY provided. Fueling skipped (Demo mode).");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const gasTank = new ethers.Wallet(gasTankKey, this.provider);
|
||||||
|
const tx = await gasTank.sendTransaction({
|
||||||
|
to: targetAddress,
|
||||||
|
value: ethers.parseEther(amount)
|
||||||
|
});
|
||||||
|
await tx.wait();
|
||||||
|
console.log(`[CryptoEngine] Fueled ${targetAddress} with ${amount} native currency. Hash: ${tx.hash}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[CryptoEngine] Fueling failed:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async sweepSolana(
|
private async sweepSolana(
|
||||||
tempWalletPrivateKey: string,
|
tempWalletPrivateKey: string,
|
||||||
merchantAddress: string,
|
merchantAddress: string,
|
||||||
@@ -123,60 +122,17 @@ export class CryptoEngine {
|
|||||||
const tempKeypair = Keypair.fromSecretKey(bs58.decode(tempWalletPrivateKey));
|
const tempKeypair = Keypair.fromSecretKey(bs58.decode(tempWalletPrivateKey));
|
||||||
const pubKey = tempKeypair.publicKey;
|
const pubKey = tempKeypair.publicKey;
|
||||||
|
|
||||||
if (tokenSymbol === 'SOL') {
|
// Check if wallet needs SOL for gas
|
||||||
const balance = await this.solConnection.getBalance(pubKey);
|
const solBalance = await this.solConnection.getBalance(pubKey);
|
||||||
if (balance === 0) throw new Error("Balance is zero");
|
if (solBalance < 5000000 && tokenSymbol !== 'SOL') {
|
||||||
|
console.log(`[Sweep SOL] Low SOL for gas, fueling...`);
|
||||||
// Leave some lamports for tx fees. Mock logic for now
|
|
||||||
const actionableBalance = balance - 5000;
|
|
||||||
const platformShare = Math.floor(actionableBalance * 0.01);
|
|
||||||
const merchantShare = actionableBalance - platformShare;
|
|
||||||
|
|
||||||
console.log(`[Sweep SOL] Total: ${balance / LAMPORTS_PER_SOL} SOL`);
|
|
||||||
console.log(`[Sweep SOL] Platform Share: ${platformShare / LAMPORTS_PER_SOL} SOL`);
|
|
||||||
console.log(`[Sweep SOL] Merchant Share: ${merchantShare / LAMPORTS_PER_SOL} SOL`);
|
|
||||||
|
|
||||||
// Off-chain transaction split using SystemProgram
|
|
||||||
const transaction = new Transaction().add(
|
|
||||||
SystemProgram.transfer({
|
|
||||||
fromPubkey: pubKey,
|
|
||||||
toPubkey: new PublicKey(platformAddress),
|
|
||||||
lamports: platformShare,
|
|
||||||
}),
|
|
||||||
SystemProgram.transfer({
|
|
||||||
fromPubkey: pubKey,
|
|
||||||
toPubkey: new PublicKey(merchantAddress),
|
|
||||||
lamports: merchantShare,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
// Uncomment the following line to actually send the transaction on devnet
|
|
||||||
// const txSig = await sendAndConfirmTransaction(this.solConnection, transaction, [tempKeypair]);
|
|
||||||
const mockTxSig = bs58.encode(ethers.randomBytes(32));
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
platformTx: mockTxSig,
|
|
||||||
merchantTx: mockTxSig
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
// Processing SPL tokens (USDC/USDT)
|
|
||||||
const tokenMint = new PublicKey(STABLECOIN_ADDRESSES['SOLANA'][tokenSymbol]);
|
|
||||||
const tempAta = await getAssociatedTokenAddress(tokenMint, pubKey);
|
|
||||||
|
|
||||||
// For real transactions we would:
|
|
||||||
// 1. Check/create ATAs for platform & merchant
|
|
||||||
// 2. Add transfer instructions
|
|
||||||
// 3. Send transaction
|
|
||||||
|
|
||||||
console.log(`[Sweep SOL SPL] Mint: ${tokenMint.toBase58()}`);
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
platformTx: 'sol_mock_tx_spl',
|
|
||||||
merchantTx: 'sol_mock_tx_spl'
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
platformTx: 'sol_mock_tx_' + Math.random().toString(36).substring(7),
|
||||||
|
merchantTx: 'sol_mock_tx_' + Math.random().toString(36).substring(7)
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -188,53 +144,36 @@ export class CryptoEngine {
|
|||||||
error?: string;
|
error?: string;
|
||||||
}> {
|
}> {
|
||||||
try {
|
try {
|
||||||
|
const tokenConfig = this.getTokenConfig(tokenSymbol || 'USDT');
|
||||||
|
if (!tokenConfig) return { success: false, error: "Token not supported in config" };
|
||||||
|
|
||||||
if (this.network === 'SOLANA') {
|
if (this.network === 'SOLANA') {
|
||||||
const pubKey = new PublicKey(address);
|
const pubKey = new PublicKey(address);
|
||||||
if (!tokenSymbol || tokenSymbol === 'SOL') {
|
if (tokenConfig.address === 'NATIVE') {
|
||||||
const balance = await this.solConnection.getBalance(pubKey);
|
const balance = await this.solConnection.getBalance(pubKey);
|
||||||
const balanceInSol = balance / LAMPORTS_PER_SOL;
|
const balanceInSol = balance / LAMPORTS_PER_SOL;
|
||||||
|
if (balanceInSol >= parseFloat(expectedAmount)) return { success: true };
|
||||||
if (balanceInSol >= parseFloat(expectedAmount)) {
|
|
||||||
return { success: true };
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
const tokenMint = new PublicKey(STABLECOIN_ADDRESSES['SOLANA'][tokenSymbol]);
|
const tokenMint = new PublicKey(tokenConfig.address);
|
||||||
const ata = await getAssociatedTokenAddress(tokenMint, pubKey);
|
const ata = await getAssociatedTokenAddress(tokenMint, pubKey);
|
||||||
try {
|
try {
|
||||||
const accountInfo = await getAccount(this.solConnection, ata);
|
const accountInfo = await getAccount(this.solConnection, ata);
|
||||||
// Using mock decimals 6 for USDT/USDC
|
const balance = Number(accountInfo.amount) / Math.pow(10, tokenConfig.decimals);
|
||||||
const balance = Number(accountInfo.amount) / 1_000_000;
|
if (balance >= parseFloat(expectedAmount)) return { success: true };
|
||||||
if (balance >= parseFloat(expectedAmount)) {
|
} catch (e) {}
|
||||||
return { success: true };
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
// Account might not exist yet
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (!tokenSymbol || tokenSymbol === 'ETH' || tokenSymbol === 'MATIC') {
|
if (tokenConfig.address === 'NATIVE') {
|
||||||
const balance = await this.provider.getBalance(address);
|
const balance = await this.provider.getBalance(address);
|
||||||
const balanceInEth = ethers.formatEther(balance);
|
const balanceInEth = ethers.formatEther(balance);
|
||||||
|
if (parseFloat(balanceInEth) >= parseFloat(expectedAmount)) return { success: true };
|
||||||
if (parseFloat(balanceInEth) >= parseFloat(expectedAmount)) {
|
|
||||||
return { success: true };
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// Check ERC20 balance (USDT/USDC)
|
const contract = new ethers.Contract(tokenConfig.address, ERC20_ABI, this.provider);
|
||||||
const tokenAddress = STABLECOIN_ADDRESSES[this.network][tokenSymbol];
|
|
||||||
if (!tokenAddress) throw new Error("Unsupported token");
|
|
||||||
|
|
||||||
const contract = new ethers.Contract(tokenAddress, ERC20_ABI, this.provider);
|
|
||||||
const balance = await contract.balanceOf(address);
|
const balance = await contract.balanceOf(address);
|
||||||
const decimals = await contract.decimals();
|
const formattedBalance = ethers.formatUnits(balance, tokenConfig.decimals);
|
||||||
const formattedBalance = ethers.formatUnits(balance, decimals);
|
if (parseFloat(formattedBalance) >= parseFloat(expectedAmount)) return { success: true };
|
||||||
|
|
||||||
if (parseFloat(formattedBalance) >= parseFloat(expectedAmount)) {
|
|
||||||
return { success: true };
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { success: false };
|
return { success: false };
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
return { success: false, error: error.message };
|
return { success: false, error: error.message };
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
import { createClient } from '@supabase/supabase-js';
|
|
||||||
|
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
|
||||||
|
|
||||||
// This should ONLY be used in Server Components or API Routes
|
|
||||||
export const supabaseAdmin = createClient(
|
|
||||||
supabaseUrl,
|
|
||||||
process.env.SUPABASE_SERVICE_ROLE_KEY!
|
|
||||||
);
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
import { createClient } from '@supabase/supabase-js';
|
|
||||||
|
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
|
||||||
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
|
||||||
|
|
||||||
export const supabase = createClient(supabaseUrl, supabaseAnonKey);
|
|
||||||
@@ -13,8 +13,6 @@
|
|||||||
"@solana/web3.js": "^1.98.4",
|
"@solana/web3.js": "^1.98.4",
|
||||||
"@stripe/react-stripe-js": "^5.4.1",
|
"@stripe/react-stripe-js": "^5.4.1",
|
||||||
"@stripe/stripe-js": "^8.6.1",
|
"@stripe/stripe-js": "^8.6.1",
|
||||||
"@supabase/ssr": "^0.8.0",
|
|
||||||
"@supabase/supabase-js": "^2.90.1",
|
|
||||||
"bcryptjs": "^3.0.3",
|
"bcryptjs": "^3.0.3",
|
||||||
"bs58": "^6.0.0",
|
"bs58": "^6.0.0",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
@@ -43,7 +41,7 @@
|
|||||||
"@types/pg": "^8.18.0",
|
"@types/pg": "^8.18.0",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
"chai": "^6.2.2",
|
"chai": "^4.3.7",
|
||||||
"eslint": "^9",
|
"eslint": "^9",
|
||||||
"eslint-config-next": "16.1.1",
|
"eslint-config-next": "16.1.1",
|
||||||
"ethers": "^6.16.0",
|
"ethers": "^6.16.0",
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
import { createBrowserClient } from '@supabase/ssr'
|
|
||||||
|
|
||||||
export function createClient() {
|
|
||||||
return createBrowserClient(
|
|
||||||
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
|
||||||
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
import { createServerClient } from '@supabase/ssr'
|
|
||||||
import { NextResponse, type NextRequest } from 'next/server'
|
|
||||||
|
|
||||||
export async function updateSession(request: NextRequest) {
|
|
||||||
let supabaseResponse = NextResponse.next({
|
|
||||||
request,
|
|
||||||
})
|
|
||||||
|
|
||||||
const supabase = createServerClient(
|
|
||||||
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
|
||||||
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
|
|
||||||
{
|
|
||||||
cookies: {
|
|
||||||
getAll() {
|
|
||||||
return request.cookies.getAll()
|
|
||||||
},
|
|
||||||
setAll(cookiesToSet) {
|
|
||||||
cookiesToSet.forEach(({ name, value, options }) => request.cookies.set(name, value))
|
|
||||||
supabaseResponse = NextResponse.next({
|
|
||||||
request,
|
|
||||||
})
|
|
||||||
cookiesToSet.forEach(({ name, value, options }) =>
|
|
||||||
supabaseResponse.cookies.set(name, value, options)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
// IMPORTANT: Avoid writing any logic between createServerClient and
|
|
||||||
// getUser(). A simple mistake can make it very hard to debug
|
|
||||||
// issues with users being logged out.
|
|
||||||
|
|
||||||
const {
|
|
||||||
data: { user },
|
|
||||||
} = await supabase.auth.getUser()
|
|
||||||
|
|
||||||
if (
|
|
||||||
!user &&
|
|
||||||
!request.nextUrl.pathname.startsWith('/login') &&
|
|
||||||
!request.nextUrl.pathname.startsWith('/auth') &&
|
|
||||||
request.nextUrl.pathname.startsWith('/admin')
|
|
||||||
) {
|
|
||||||
// no user, potentially respond by redirecting the user to the login page
|
|
||||||
const url = request.nextUrl.clone()
|
|
||||||
url.pathname = '/login'
|
|
||||||
return NextResponse.redirect(url)
|
|
||||||
}
|
|
||||||
|
|
||||||
// IMPORTANT: You *must* return the supabaseResponse object as is. If you're creating a
|
|
||||||
// new response object with NextResponse.next() make sure to:
|
|
||||||
// 1. Pass the request in it, like so:
|
|
||||||
// const myNewResponse = NextResponse.next({ request })
|
|
||||||
// 2. Copy over the cookies, like so:
|
|
||||||
// myNewResponse.cookies.setAll(supabaseResponse.cookies.getAll())
|
|
||||||
// 3. Change the myNewResponse object to fit your needs, but make sure to return it!
|
|
||||||
// If you don't, you can accidentally upend the user's session.
|
|
||||||
|
|
||||||
return supabaseResponse
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
import { createServerClient } from '@supabase/ssr'
|
|
||||||
import { cookies } from 'next/headers'
|
|
||||||
|
|
||||||
export async function createClient() {
|
|
||||||
const cookieStore = await cookies()
|
|
||||||
|
|
||||||
return createServerClient(
|
|
||||||
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
|
||||||
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
|
|
||||||
{
|
|
||||||
cookies: {
|
|
||||||
getAll() {
|
|
||||||
return cookieStore.getAll()
|
|
||||||
},
|
|
||||||
setAll(cookiesToSet) {
|
|
||||||
try {
|
|
||||||
cookiesToSet.forEach(({ name, value, options }) =>
|
|
||||||
cookieStore.set(name, value, options)
|
|
||||||
)
|
|
||||||
} catch {
|
|
||||||
// The `setAll` method was called from a Server Component.
|
|
||||||
// This can be ignored if you have middleware refreshing
|
|
||||||
// user sessions.
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user