Implement Pakasir Phase 1
Add env-based Pakasir API key with fallback, create orders + transaction via Pakasir, and update checkout UI to reflect payment status and access flow. Centralize key retrieval, include placeholder callback URL, and adjust UI to show processing state and Indonesian messaging. Enable client-side access checks for paid orders in product detail and member dashboard (without edge function). X-Lovable-Edit-ID: edt-14f89c00-5022-4166-bb29-df4832b81add
This commit is contained in:
@@ -15,7 +15,15 @@ import { QRCodeSVG } from "qrcode.react";
|
||||
|
||||
// Pakasir configuration
|
||||
const PAKASIR_PROJECT_SLUG = "dewengoding";
|
||||
const PAKASIR_API_KEY = "iP13osgh7lAzWWIPsj7TbW5M3iGEAQMo";
|
||||
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;
|
||||
};
|
||||
|
||||
// TODO: Replace with actual Supabase Edge Function URL after creation
|
||||
const PAKASIR_CALLBACK_URL = "https://lovable.backoffice.biz.id/functions/v1/pakasir-webhook";
|
||||
|
||||
type PaymentMethod = "qris" | "paypal";
|
||||
type CheckoutStep = "cart" | "payment" | "waiting";
|
||||
@@ -117,6 +125,9 @@ export default function Checkout() {
|
||||
|
||||
setOrderId(order.id);
|
||||
|
||||
// Build description from product titles
|
||||
const productTitles = items.map(item => item.title).join(", ");
|
||||
|
||||
if (paymentMethod === "qris") {
|
||||
// Call Pakasir API for QRIS
|
||||
try {
|
||||
@@ -127,7 +138,9 @@ export default function Checkout() {
|
||||
project: PAKASIR_PROJECT_SLUG,
|
||||
order_id: order.id,
|
||||
amount: amountInRupiah,
|
||||
api_key: PAKASIR_API_KEY,
|
||||
api_key: getPakasirApiKey(),
|
||||
description: productTitles,
|
||||
callback_url: PAKASIR_CALLBACK_URL,
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -222,7 +235,7 @@ export default function Checkout() {
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground text-center">
|
||||
Pembayaran akan otomatis terkonfirmasi setelah Anda scan dan bayar QR code di atas
|
||||
Pembayaran diproses melalui Pakasir dan akan dikonfirmasi otomatis setelah berhasil.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -121,13 +121,35 @@ export default function ProductDetail() {
|
||||
|
||||
const checkUserAccess = async () => {
|
||||
if (!product || !user) return;
|
||||
const { data } = await supabase
|
||||
|
||||
// Check user_access table first
|
||||
const { data: accessData } = await supabase
|
||||
.from('user_access')
|
||||
.select('id')
|
||||
.eq('user_id', user.id)
|
||||
.eq('product_id', product.id)
|
||||
.maybeSingle();
|
||||
setHasAccess(!!data);
|
||||
|
||||
if (accessData) {
|
||||
setHasAccess(true);
|
||||
setCheckingAccess(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Also check for paid orders containing this product
|
||||
const { data: paidOrders } = await supabase
|
||||
.from('orders')
|
||||
.select(`
|
||||
id,
|
||||
order_items!inner (product_id)
|
||||
`)
|
||||
.eq('user_id', user.id)
|
||||
.eq('payment_status', 'paid')
|
||||
.eq('payment_provider', 'pakasir')
|
||||
.eq('order_items.product_id', product.id)
|
||||
.limit(1);
|
||||
|
||||
setHasAccess(!!(paidOrders && paidOrders.length > 0));
|
||||
setCheckingAccess(false);
|
||||
};
|
||||
|
||||
|
||||
@@ -42,11 +42,38 @@ export default function MemberDashboard() {
|
||||
}, [user, authLoading]);
|
||||
|
||||
const fetchData = async () => {
|
||||
const [accessRes, ordersRes] = await Promise.all([
|
||||
const [accessRes, ordersRes, paidOrdersRes] = await Promise.all([
|
||||
supabase.from('user_access').select(`id, product:products (id, title, slug, type, meeting_link, recording_url)`).eq('user_id', user!.id),
|
||||
supabase.from('orders').select('*').eq('user_id', user!.id).order('created_at', { ascending: false }).limit(3)
|
||||
supabase.from('orders').select('*').eq('user_id', user!.id).order('created_at', { ascending: false }).limit(3),
|
||||
// Also get products from paid orders (via order_items)
|
||||
supabase.from('orders')
|
||||
.select(`
|
||||
order_items (
|
||||
product:products (id, title, slug, type, meeting_link, recording_url)
|
||||
)
|
||||
`)
|
||||
.eq('user_id', user!.id)
|
||||
.eq('payment_status', 'paid')
|
||||
.eq('payment_provider', 'pakasir')
|
||||
]);
|
||||
if (accessRes.data) setAccess(accessRes.data as unknown as UserAccess[]);
|
||||
|
||||
// Combine access from user_access and paid orders
|
||||
const directAccess = accessRes.data as unknown as UserAccess[] || [];
|
||||
const paidProductAccess: UserAccess[] = [];
|
||||
|
||||
if (paidOrdersRes.data) {
|
||||
const existingIds = new Set(directAccess.map(a => a.product.id));
|
||||
paidOrdersRes.data.forEach((order: any) => {
|
||||
order.order_items?.forEach((item: any) => {
|
||||
if (item.product && !existingIds.has(item.product.id)) {
|
||||
existingIds.add(item.product.id);
|
||||
paidProductAccess.push({ id: `paid-${item.product.id}`, product: item.product });
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
setAccess([...directAccess, ...paidProductAccess]);
|
||||
if (ordersRes.data) setRecentOrders(ordersRes.data);
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user