"use client"; import { Logger } from "@/utils/logger"; import { Result } from "@/utils/result"; import { savePendingEzpayOrder, type PendingPaymentReturnTo, type PendingPaymentSubscriptionType, } from "./pending_payment_order"; const log = new Logger("LibPaymentPaymentLaunch"); export type EzpayLaunchTarget = | { kind: "url"; paymentUrl: string } | { kind: "qr"; experience: EzpayQrExperience; qrData: string; } | { kind: "error"; errorMessage: string }; export type EzpayQrExperience = "gcashQrPh" | "qris" | "paymentQr"; export interface ResolveEzpayLaunchContext { countryCode?: string | null; currency?: string | null; } function getNonEmptyString( payParams: Record, ...keys: string[] ): string | null { for (const key of keys) { const value = payParams[key]; if (typeof value === "string" && value.trim().length > 0) { return value.trim(); } } return null; } function getHttpPaymentUrl(value: string | null): string | null { if (!value) return null; try { const parsed = new URL(value); return parsed.protocol === "https:" || parsed.protocol === "http:" ? value : null; } catch { return null; } } export function getPaymentUrlHostname(value: string | null): string | null { const paymentUrl = getHttpPaymentUrl(value); if (!paymentUrl) return null; return new URL(paymentUrl).hostname; } export function getPaymentUrl(payParams: Record): string | null { const keys = [ "cashierUrl", "cashier_url", "checkoutUrl", "checkout_url", "paymentUrl", "payment_url", "approvalUrl", "approval_url", "redirectUrl", "redirect_url", "url", ]; for (const key of keys) { const value = payParams[key]; if (typeof value === "string" && value.length > 0) return value; } return null; } export function isEzpayPayment(payParams: Record): boolean { const provider = payParams.provider; return typeof provider === "string" && provider.toLowerCase() === "ezpay"; } export function resolveEzpayLaunchTarget( payParams: Record, context: ResolveEzpayLaunchContext = {}, ): EzpayLaunchTarget { const rawChannelType = getNonEmptyString( payParams, "channelType", "channel_type", ); const channelType = rawChannelType?.toUpperCase() ?? null; const payData = getNonEmptyString(payParams, "payData", "pay_data"); const payDataUrl = getHttpPaymentUrl(payData); const namedPaymentUrl = getHttpPaymentUrl(getPaymentUrl(payParams)); const paymentUrl = payDataUrl ?? namedPaymentUrl; const qrData = payData && !payDataUrl ? payData : null; const region = resolveEzpayRegion(payParams, context); if ( region === "ph" && paymentUrl && (channelType === "QR" || channelType === "URL" || channelType === null) ) { return { kind: "url", paymentUrl }; } if (channelType === "QR") { if (qrData) { return { kind: "qr", experience: resolveEzpayQrExperience(region), qrData, }; } return paymentUrl ? { kind: "url", paymentUrl } : { kind: "error", errorMessage: regionalQrMissingMessage(region), }; } if (channelType === "VA") { return { kind: "error", errorMessage: "Virtual account payments are not supported in this app.", }; } if (channelType === "URL") { return paymentUrl ? { kind: "url", paymentUrl } : { kind: "error", errorMessage: "Ezpay payment URL is missing or invalid. Please try again.", }; } if (channelType) { return { kind: "error", errorMessage: `Unsupported Ezpay payment channel: ${channelType}.`, }; } if (paymentUrl) return { kind: "url", paymentUrl }; if (qrData && region !== null) { return { kind: "qr", experience: resolveEzpayQrExperience(region), qrData, }; } return { kind: "error", errorMessage: "Ezpay payment parameters did not include a supported URL or payment QR code.", }; } function resolveEzpayRegion( payParams: Record, context: ResolveEzpayLaunchContext, ): "ph" | "id" | null { const currency = ( context.currency ?? getNonEmptyString(payParams, "currency") ) ?.trim() .toUpperCase(); if (currency === "PHP") return "ph"; if (currency === "IDR") return "id"; const countryCode = ( context.countryCode ?? getNonEmptyString( payParams, "purchaseCountryCode", "purchase_country_code", "countryCode", "country_code", ) ) ?.trim() .toUpperCase(); if (countryCode === "PH") return "ph"; if (countryCode === "ID") return "id"; return null; } function resolveEzpayQrExperience( region: "ph" | "id" | null, ): EzpayQrExperience { if (region === "ph") return "gcashQrPh"; if (region === "id") return "qris"; return "paymentQr"; } function regionalQrMissingMessage(region: "ph" | "id" | null): string { if (region === "ph") { return "GCash / QR Ph payment data is missing. Please try again."; } if (region === "id") { return "QRIS payment data is missing. Please try again."; } return "Payment QR data is missing. Please try again."; } export function getStripeClientSecret( payParams: Record, ): string | null { const provider = payParams.provider; const clientSecret = payParams.clientSecret ?? payParams.client_secret; const isStripeProvider = typeof provider !== "string" || provider.toLowerCase() === "stripe"; if ( isStripeProvider && typeof clientSecret === "string" && clientSecret.length > 0 ) { return clientSecret; } return null; } export interface LaunchEzpayRedirectInput { orderId: string | null; paymentUrl: string; subscriptionType: PendingPaymentSubscriptionType; giftCategory?: string | null; giftPlanId?: string | null; returnTo?: PendingPaymentReturnTo; characterSlug?: string; onOpened?: () => void; onFailed: (errorMessage: string) => void; } export async function launchEzpayRedirect({ orderId, paymentUrl, subscriptionType, giftCategory, giftPlanId, returnTo, characterSlug, onOpened, onFailed, }: LaunchEzpayRedirectInput): Promise { const paymentUrlHost = getPaymentUrlHostname(paymentUrl); log.debug("[payment-launch] launchEzpayRedirect START", { hasOrderId: Boolean(orderId), orderId, subscriptionType, paymentUrlHost, }); if (!orderId) { const errorMessage = "Missing order id before opening Ezpay."; log.error("[payment-launch] pending ezpay order save skipped", { subscriptionType, paymentUrlHost, errorMessage, }); onFailed(errorMessage); return; } const saveResult = await savePendingEzpayOrder({ orderId, subscriptionType, giftCategory, giftPlanId, ...(returnTo ? { returnTo } : {}), ...(characterSlug ? { characterSlug } : {}), }); if (Result.isErr(saveResult)) { const errorMessage = "Could not save payment order before opening Ezpay. Please try again."; log.error("[payment-launch] pending ezpay order save failed", { orderId, subscriptionType, error: saveResult.error, }); onFailed(errorMessage); return; } log.debug("[payment-launch] pending ezpay order saved", { orderId, subscriptionType, }); log.debug("[payment-launch] launchEzpayRedirect NOW", { orderId, subscriptionType, paymentUrl, }); try { window.location.href = paymentUrl; onOpened?.(); } catch { onFailed("Could not open Ezpay. Please try again."); } }