From a9f7c9b07a468f8945af0e2aef91685eaabb004a Mon Sep 17 00:00:00 2001 From: dwindown Date: Tue, 23 Dec 2025 21:20:40 +0700 Subject: [PATCH] Create Pakasir payment edge function to fix CORS issue MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Create create-pakasir-payment edge function to handle payment creation server-side - Update ConsultingBooking.tsx to use edge function instead of direct API call - Update Checkout.tsx to use edge function instead of direct API call - Add config.toml entry for create-pakasir-payment function - Removes CORS errors when calling Pakasir API from frontend 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- src/pages/Checkout.tsx | 59 +++++----- src/pages/ConsultingBooking.tsx | 47 +++----- supabase/config.toml | 3 + .../functions/create-pakasir-payment/index.ts | 102 ++++++++++++++++++ 4 files changed, 147 insertions(+), 64 deletions(-) create mode 100644 supabase/functions/create-pakasir-payment/index.ts diff --git a/src/pages/Checkout.tsx b/src/pages/Checkout.tsx index abbbeee..35cc9bd 100644 --- a/src/pages/Checkout.tsx +++ b/src/pages/Checkout.tsx @@ -129,46 +129,37 @@ export default function Checkout() { setOrderId(order.id); - // Build description from product titles - const productTitles = items.map(item => item.title).join(", "); + // Build description from product titles + const productTitles = items.map(item => item.title).join(", "); - if (paymentMethod === "qris") { - // Call Pakasir API for QRIS - try { - const response = await fetch(`https://app.pakasir.com/api/transactioncreate/qris`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - project: PAKASIR_PROJECT_SLUG, - order_id: order.id, - amount: amountInRupiah, - api_key: getPakasirApiKey(), - description: productTitles, - callback_url: PAKASIR_CALLBACK_URL, - }), - }); + if (paymentMethod === "qris") { + // Call edge function to create Pakasir QRIS payment (avoids CORS) + const { data: paymentData, error: paymentError } = await supabase.functions.invoke('create-pakasir-payment', { + body: { + order_id: order.id, + amount: amountInRupiah, + description: productTitles, + }, + }); - const result = await response.json(); - - if (result.qr_string || result.qr) { - setPaymentData({ - qr_string: result.qr_string || result.qr, - expired_at: result.expired_at, - order_id: order.id, - }); - setStep("waiting"); - clearCart(); - } else { - // Fallback to redirect if API doesn't return QR - const pakasirUrl = `https://app.pakasir.com/pay/${PAKASIR_PROJECT_SLUG}/${amountInRupiah}?order_id=${order.id}`; - clearCart(); - window.location.href = pakasirUrl; - } - } catch { + if (paymentError) { + console.error('Payment creation error:', paymentError); // Fallback to redirect const pakasirUrl = `https://app.pakasir.com/pay/${PAKASIR_PROJECT_SLUG}/${amountInRupiah}?order_id=${order.id}`; clearCart(); window.location.href = pakasirUrl; + } else if (paymentData?.data?.qr_string) { + // QRIS available - show QR code + 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 + window.location.href = paymentData.data.payment_url; } } else { // PayPal - redirect to Pakasir PayPal URL diff --git a/src/pages/ConsultingBooking.tsx b/src/pages/ConsultingBooking.tsx index 47cfa09..202592c 100644 --- a/src/pages/ConsultingBooking.tsx +++ b/src/pages/ConsultingBooking.tsx @@ -226,38 +226,25 @@ export default function ConsultingBooking() { const { error: slotsError } = await supabase.from('consulting_slots').insert(slotsToInsert); if (slotsError) throw slotsError; - // Call Pakasir API directly to create QRIS payment - const PAKASIR_PROJECT_SLUG = import.meta.env.VITE_PAKASIR_PROJECT_SLUG || ''; - const PAKASIR_API_KEY = import.meta.env.VITE_PAKASIR_API_KEY || ''; - const PAKASIR_CALLBACK_URL = `${import.meta.env.VITE_SUPABASE_URL}/functions/v1/pakasir-webhook`; + // Call edge function to create Pakasir payment (avoids CORS) + const { data: paymentData, error: paymentError } = await supabase.functions.invoke('create-pakasir-payment', { + body: { + order_id: order.id, + amount: totalPrice, + description: `Konsultasi 1-on-1 (${totalBlocks} blok)`, + }, + }); - try { - const response = await fetch(`https://app.pakasir.com/api/transactioncreate/qris`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - project: PAKASIR_PROJECT_SLUG, - order_id: order.id, - amount: totalPrice, - api_key: PAKASIR_API_KEY, - description: `Konsultasi 1-on-1 (${totalBlocks} blok)`, - callback_url: PAKASIR_CALLBACK_URL, - }), - }); + if (paymentError) { + console.error('Payment creation error:', paymentError); + throw new Error(paymentError.message || 'Gagal membuat pembayaran'); + } - const result = await response.json(); - - if (result.qr_string || result.qr) { - // QRIS available - redirect to Pakasir payment page - const pakasirPayUrl = `https://app.pakasir.com/pay/${PAKASIR_PROJECT_SLUG}/${totalPrice}?order_id=${order.id}`; - window.location.href = pakasirPayUrl; - } else { - throw new Error('Failed to create payment'); - } - } catch (pakasirError) { - // Fallback: redirect directly to Pakasir - const pakasirPayUrl = `https://app.pakasir.com/pay/${PAKASIR_PROJECT_SLUG}/${totalPrice}?order_id=${order.id}`; - window.location.href = pakasirPayUrl; + if (paymentData?.success && paymentData?.data?.payment_url) { + // Redirect to Pakasir payment page + window.location.href = paymentData.data.payment_url; + } else { + throw new Error('Gagal membuat URL pembayaran'); } } catch (error: any) { toast({ title: 'Error', description: error.message, variant: 'destructive' }); diff --git a/supabase/config.toml b/supabase/config.toml index cb068d4..82c28a7 100644 --- a/supabase/config.toml +++ b/supabase/config.toml @@ -44,3 +44,6 @@ verify_jwt = false [functions.delete-order] verify_jwt = false + +[functions.create-pakasir-payment] +verify_jwt = false diff --git a/supabase/functions/create-pakasir-payment/index.ts b/supabase/functions/create-pakasir-payment/index.ts new file mode 100644 index 0000000..50e5091 --- /dev/null +++ b/supabase/functions/create-pakasir-payment/index.ts @@ -0,0 +1,102 @@ +import { serve } from "https://deno.land/std@0.190.0/http/server.ts"; + +const corsHeaders = { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Headers": "authorization, x-client-info, apikey, content-type", +}; + +interface PakasirPaymentRequest { + order_id: string; + amount: number; + description: string; +} + +serve(async (req: Request) => { + // Handle CORS preflight + if (req.method === "OPTIONS") { + return new Response(null, { headers: corsHeaders }); + } + + if (req.method !== "POST") { + return new Response("Method not allowed", { status: 405, headers: corsHeaders }); + } + + try { + const body: PakasirPaymentRequest = await req.json(); + const { order_id, amount, description } = body; + + if (!order_id || !amount) { + return new Response( + JSON.stringify({ success: false, error: "order_id and amount are required" }), + { status: 400, headers: { ...corsHeaders, "Content-Type": "application/json" } } + ); + } + + const PAKASIR_PROJECT_SLUG = Deno.env.get("PAKASIR_PROJECT_SLUG") || ""; + const PAKASIR_API_KEY = Deno.env.get("PAKASIR_API_KEY") || ""; + const PAKASIR_CALLBACK_URL = `${Deno.env.get("SUPABASE_URL")}/functions/v1/pakasir-webhook`; + + if (!PAKASIR_PROJECT_SLUG || !PAKASIR_API_KEY) { + console.error("[PAKASIR] Missing credentials"); + return new Response( + JSON.stringify({ success: false, error: "Pakasir credentials not configured" }), + { status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" } } + ); + } + + console.log("[PAKASIR] Creating payment transaction:", { order_id, amount }); + + // Call Pakasir API to create QRIS transaction + const pakasirResponse = await fetch(`https://app.pakasir.com/api/transactioncreate/qris`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + project: PAKASIR_PROJECT_SLUG, + order_id: order_id, + amount: amount, + api_key: PAKASIR_API_KEY, + description: description || `Order ${order_id}`, + callback_url: PAKASIR_CALLBACK_URL, + }), + }); + + if (!pakasirResponse.ok) { + const errorText = await pakasirResponse.text(); + console.error("[PAKASIR] API error:", pakasirResponse.status, errorText); + return new Response( + JSON.stringify({ + success: false, + error: "Pakasir API error", + details: errorText + }), + { status: pakasirResponse.status, headers: { ...corsHeaders, "Content-Type": "application/json" } } + ); + } + + const result = await pakasirResponse.json(); + console.log("[PAKASIR] Payment created:", result); + + // Return payment URL and QR data + return new Response( + JSON.stringify({ + success: true, + data: { + payment_url: `https://app.pakasir.com/pay/${PAKASIR_PROJECT_SLUG}/${amount}?order_id=${order_id}`, + qr_string: result.qr_string || result.qr || null, + order_id: order_id, + } + }), + { headers: { ...corsHeaders, "Content-Type": "application/json" } } + ); + + } catch (error: any) { + console.error("[PAKASIR] Unexpected error:", error); + return new Response( + JSON.stringify({ + success: false, + error: error.message || "Internal server error" + }), + { status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" } } + ); + } +});