diff --git a/src/pages/Checkout.tsx b/src/pages/Checkout.tsx index d74ddfd..03f848b 100644 --- a/src/pages/Checkout.tsx +++ b/src/pages/Checkout.tsx @@ -1,26 +1,14 @@ -import { useState, useEffect } from "react"; -import { useNavigate, useSearchParams } from "react-router-dom"; +import { useState } from "react"; +import { useNavigate } from "react-router-dom"; import { AppLayout } from "@/components/AppLayout"; import { useCart } from "@/contexts/CartContext"; import { useAuth } from "@/hooks/useAuth"; import { supabase } from "@/integrations/supabase/client"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; -import { Label } from "@/components/ui/label"; import { toast } from "@/hooks/use-toast"; import { formatIDR } from "@/lib/format"; -import { Trash2, CreditCard, Loader2, QrCode, Wallet } from "lucide-react"; -import { QRCodeSVG } from "qrcode.react"; - -// Pakasir configuration -const PAKASIR_PROJECT_SLUG = import.meta.env.VITE_PAKASIR_PROJECT_SLUG || "dewengoding"; -const SANDBOX_API_KEY = "iP13osgh7lAzWWIPsj7TbW5M3iGEAQMo"; - -// Centralized API key retrieval - uses env var with sandbox fallback -const getPakasirApiKey = (): string => { - return import.meta.env.VITE_PAKASIR_API_KEY || SANDBOX_API_KEY; -}; +import { Trash2, CreditCard, Loader2, QrCode } from "lucide-react"; // Edge function base URL - configurable via env with sensible default const getEdgeFunctionBaseUrl = (): string => { @@ -29,49 +17,23 @@ const getEdgeFunctionBaseUrl = (): string => { const PAKASIR_CALLBACK_URL = `${getEdgeFunctionBaseUrl()}/pakasir-webhook`; -type PaymentMethod = "qris" | "paypal"; -type CheckoutStep = "cart" | "payment" | "waiting"; - -interface PaymentData { - qr_string?: string; - payment_url?: string; - expired_at?: string; - order_id?: string; -} +type CheckoutStep = "cart" | "payment"; export default function Checkout() { const { items, removeItem, clearCart, total } = useCart(); const { user } = useAuth(); const navigate = useNavigate(); - const [searchParams] = useSearchParams(); const [loading, setLoading] = useState(false); const [step, setStep] = useState("cart"); - const [paymentMethod, setPaymentMethod] = useState("qris"); - const [paymentData, setPaymentData] = useState(null); - const [orderId, setOrderId] = useState(null); - const [checkingStatus, setCheckingStatus] = useState(false); - - // Check for returning from PayPal - useEffect(() => { - const returnedOrderId = searchParams.get("order_id"); - if (returnedOrderId) { - setOrderId(returnedOrderId); - checkPaymentStatus(returnedOrderId); - } - }, [searchParams]); const checkPaymentStatus = async (oid: string) => { - setCheckingStatus(true); const { data: order } = await supabase.from("orders").select("payment_status").eq("id", oid).single(); if (order?.payment_status === "paid") { toast({ title: "Pembayaran berhasil!", description: "Akses produk sudah aktif" }); navigate(`/orders/${oid}`); - } else { - toast({ title: "Pembayaran pending", description: "Menunggu konfirmasi pembayaran" }); } - setCheckingStatus(false); }; const handleCheckout = async () => { @@ -107,7 +69,7 @@ export default function Checkout() { payment_provider: "pakasir", payment_reference: orderRef, payment_status: "pending", - payment_method: paymentMethod, + payment_method: "qris", }) .select() .single(); @@ -127,18 +89,15 @@ export default function Checkout() { const { error: itemsError } = await supabase.from("order_items").insert(orderItems); if (itemsError) throw new Error("Gagal menambahkan item order"); - setOrderId(order.id); - // Build description from product titles const productTitles = items.map(item => item.title).join(", "); - // Call edge function to create payment (avoids CORS) + // Call edge function to create QRIS payment const { data: paymentData, error: paymentError } = await supabase.functions.invoke('create-payment', { body: { order_id: order.id, amount: amountInRupiah, description: productTitles, - method: paymentMethod, // 'qris' or 'paypal' }, }); @@ -147,28 +106,9 @@ export default function Checkout() { throw new Error(paymentError.message || 'Gagal membuat pembayaran'); } - if (paymentData?.success) { - if (paymentData.data.method === 'paypal') { - // PayPal - redirect to payment URL - clearCart(); - window.location.href = paymentData.data.payment_url; - } else if (paymentData.data.qr_string) { - // QRIS available - show QR code in app - setPaymentData({ - qr_string: paymentData.data.qr_string, - expired_at: paymentData.data.expired_at, - order_id: order.id, - }); - setStep("waiting"); - clearCart(); - } else { - // No QR code - redirect to payment page - clearCart(); - window.location.href = paymentData.data.payment_url; - } - } else { - throw new Error('Gagal membuat pembayaran'); - } + // Clear cart and redirect to order detail page to show QR code + clearCart(); + navigate(`/orders/${order.id}`); } catch (error) { console.error("Checkout error:", error); toast({ @@ -182,28 +122,13 @@ export default function Checkout() { }; const refreshPaymentStatus = async () => { - if (!orderId) return; - setCheckingStatus(true); - - const { data: order } = await supabase.from("orders").select("payment_status").eq("id", orderId).single(); - - if (order?.payment_status === "paid") { - toast({ title: "Pembayaran berhasil!", description: "Akses produk sudah aktif" }); - navigate(`/orders/${orderId}`); - } else { - toast({ title: "Belum ada pembayaran", description: "Silakan selesaikan pembayaran" }); - } - setCheckingStatus(false); + // This function is now handled in OrderDetail page + // Kept for backwards compatibility but no longer used + toast({ title: "Info", description: "Status pembayaran diupdate otomatis" }); }; - // Waiting for QRIS payment - if (step === "waiting" && paymentData) { - return ( - -
- - - Scan QR Code untuk Bayar + // Payment method selection UI - QRIS only now + if (step === "payment") {
@@ -282,34 +207,15 @@ export default function Checkout() { Metode Pembayaran - setPaymentMethod(v as PaymentMethod)} - className="space-y-3" - > -
- - +
+ +
+

QRIS

+

+ Scan QR dengan aplikasi e-wallet atau mobile banking +

-
- - -
- +
@@ -333,7 +239,7 @@ export default function Checkout() { ) : user ? ( <> - Bayar dengan {paymentMethod === "qris" ? "QRIS" : "PayPal"} + Bayar dengan QRIS ) : ( "Login untuk Checkout" diff --git a/supabase/functions/create-payment/index.ts b/supabase/functions/create-payment/index.ts index 6a260cb..f3bc963 100644 --- a/supabase/functions/create-payment/index.ts +++ b/supabase/functions/create-payment/index.ts @@ -1,4 +1,5 @@ import { serve } from "https://deno.land/std@0.190.0/http/server.ts"; +import { createClient } from "https://esm.sh/@supabase/supabase-js@2"; const corsHeaders = { "Access-Control-Allow-Origin": "*", @@ -9,7 +10,6 @@ interface CreatePaymentRequest { order_id: string; amount: number; description: string; - method?: 'qris' | 'paypal'; } serve(async (req: Request) => { @@ -24,7 +24,7 @@ serve(async (req: Request) => { try { const body: CreatePaymentRequest = await req.json(); - const { order_id, amount, description, method = 'qris' } = body; + const { order_id, amount, description } = body; if (!order_id || !amount) { return new Response( @@ -36,6 +36,10 @@ serve(async (req: Request) => { const PAYMENT_PROJECT_SLUG = Deno.env.get("PAKASIR_PROJECT_SLUG") || ""; const PAYMENT_API_KEY = Deno.env.get("PAKASIR_API_KEY") || ""; const PAYMENT_CALLBACK_URL = `${Deno.env.get("SUPABASE_URL")}/functions/v1/pakasir-webhook`; + const supabase = createClient( + Deno.env.get("SUPABASE_URL")!, + Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")! + ); if (!PAYMENT_PROJECT_SLUG || !PAYMENT_API_KEY) { console.error("[PAYMENT] Missing credentials"); @@ -45,26 +49,9 @@ serve(async (req: Request) => { ); } - console.log("[PAYMENT] Creating payment transaction:", { order_id, amount, method }); + console.log("[PAYMENT] Creating QRIS transaction:", { order_id, amount }); - if (method === 'paypal') { - // Return PayPal payment URL - const paypalUrl = `https://app.pakasir.com/paypal/${PAYMENT_PROJECT_SLUG}/${amount}?order_id=${order_id}`; - - return new Response( - JSON.stringify({ - success: true, - data: { - payment_url: paypalUrl, - method: 'paypal', - order_id: order_id, - } - }), - { headers: { ...corsHeaders, "Content-Type": "application/json" } } - ); - } - - // Default: QRIS - Call provider API + // Call Pakasir API to create QRIS transaction const paymentResponse = await fetch(`https://app.pakasir.com/api/transactioncreate/qris`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, @@ -90,7 +77,6 @@ serve(async (req: Request) => { success: true, data: { payment_url: fallbackUrl, - method: 'qris', fallback: true, order_id: order_id, } @@ -102,14 +88,37 @@ serve(async (req: Request) => { const result = await paymentResponse.json(); console.log("[PAYMENT] Payment created:", result); + // Extract QR data from response + const qrData = result.payment || result; + const qrString = qrData.payment_number || qrData.qr_string || null; + const expiresAt = qrData.expired_at || null; + + // Store QR string in database for in-app display + if (qrString) { + const { error: updateError } = await supabase + .from("orders") + .update({ + qr_string: qrString, + qr_expires_at: expiresAt, + }) + .eq("id", order_id); + + if (updateError) { + console.error("[PAYMENT] Failed to store QR string:", updateError); + // Don't fail the request, just log the error + } else { + console.log("[PAYMENT] QR string stored in database"); + } + } + // Return QRIS data for display in app return new Response( JSON.stringify({ success: true, data: { method: 'qris', - qr_string: result.qr_string || result.qr || null, - expired_at: result.expired_at || null, + qr_string: qrString, + expired_at: expiresAt, // Fallback URL if QR code is not available payment_url: `https://app.pakasir.com/pay/${PAYMENT_PROJECT_SLUG}/${amount}?order_id=${order_id}`, order_id: order_id, diff --git a/supabase/functions/pakasir-webhook/index.ts b/supabase/functions/pakasir-webhook/index.ts index 50c94e4..98a6602 100644 --- a/supabase/functions/pakasir-webhook/index.ts +++ b/supabase/functions/pakasir-webhook/index.ts @@ -95,6 +95,9 @@ serve(async (req) => { payment_provider: "pakasir", payment_method: payload.payment_method || "unknown", updated_at: new Date().toISOString(), + // Clear QR string after payment + qr_string: null, + qr_expires_at: null, }) .eq("id", order.id); diff --git a/supabase/migrations/20241224_add_qr_string_to_orders.sql b/supabase/migrations/20241224_add_qr_string_to_orders.sql new file mode 100644 index 0000000..6398cac --- /dev/null +++ b/supabase/migrations/20241224_add_qr_string_to_orders.sql @@ -0,0 +1,19 @@ +-- ============================================================================ +-- Add QR String Support to Orders +-- ============================================================================ +-- Stores QRIS string and expiry for in-app QR code display +-- Eliminates need to redirect to external payment page +-- ============================================================================ + +-- Add columns for QRIS payment data +ALTER TABLE orders +ADD COLUMN IF NOT EXISTS qr_string TEXT, +ADD COLUMN IF NOT EXISTS qr_expires_at TIMESTAMPTZ; + +-- Add index for cleanup of expired QR codes +CREATE INDEX IF NOT EXISTS idx_orders_qr_expires_at ON orders(qr_expires_at) +WHERE qr_expires_at IS NOT NULL; + +-- Add comment +COMMENT ON COLUMN orders.qr_string IS 'QRIS payment string for generating QR code in-app. Cleared after payment or expiration.'; +COMMENT ON COLUMN orders.qr_expires_at IS 'QRIS code expiration timestamp. Used for cleanup and validation.';