fix(payment): send result redirects to the frontend + add /payment/result page
This commit is contained in:
@@ -88,6 +88,9 @@ services:
|
|||||||
Jwt__Audience: "flatrender"
|
Jwt__Audience: "flatrender"
|
||||||
Jwt__AccessTokenMinutes: "${JWT_ACCESS_MINUTES:-1440}"
|
Jwt__AccessTokenMinutes: "${JWT_ACCESS_MINUTES:-1440}"
|
||||||
ServiceToken: "${SERVICE_TOKEN:-internal-service-secret}"
|
ServiceToken: "${SERVICE_TOKEN:-internal-service-secret}"
|
||||||
|
# Payment callbacks land on this service (api.*); the result page is on the
|
||||||
|
# frontend. Used to make /payment/result redirects absolute to the site.
|
||||||
|
Frontend__BaseUrl: "${NEXT_PUBLIC_SITE_URL:-http://localhost:3000}"
|
||||||
ZarinPal__MerchantId: "${ZARINPAL_MERCHANT_ID:-}"
|
ZarinPal__MerchantId: "${ZARINPAL_MERCHANT_ID:-}"
|
||||||
ZarinPal__CallbackUrl: "${ZARINPAL_CALLBACK_URL:-http://localhost:8080/v1/payments/callback/zarinpal}"
|
ZarinPal__CallbackUrl: "${ZARINPAL_CALLBACK_URL:-http://localhost:8080/v1/payments/callback/zarinpal}"
|
||||||
ZarinPal__Sandbox: "${ZARINPAL_SANDBOX:-true}"
|
ZarinPal__Sandbox: "${ZARINPAL_SANDBOX:-true}"
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ namespace FlatRender.IdentitySvc.Controllers;
|
|||||||
|
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("v1")]
|
[Route("v1")]
|
||||||
public class PaymentsController(IPaymentService paymentService) : ControllerBase
|
public class PaymentsController(IPaymentService paymentService, IConfiguration config) : ControllerBase
|
||||||
{
|
{
|
||||||
// ── Helpers ───────────────────────────────────────────────────────────────────
|
// ── Helpers ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -19,6 +19,18 @@ public class PaymentsController(IPaymentService paymentService) : ControllerBase
|
|||||||
|
|
||||||
private bool IsAdmin => User.FindFirst("is_admin")?.Value == "true";
|
private bool IsAdmin => User.FindFirst("is_admin")?.Value == "true";
|
||||||
|
|
||||||
|
// Payment callbacks land on this service (api.flatrender.ir); the result page
|
||||||
|
// lives on the frontend. Prefix relative result paths with the frontend base so
|
||||||
|
// the browser is sent to the site, not the gateway.
|
||||||
|
private IActionResult RedirectFrontend(string path)
|
||||||
|
{
|
||||||
|
var baseUrl = config["Frontend:BaseUrl"] ?? "";
|
||||||
|
var target = string.IsNullOrEmpty(baseUrl) || path.StartsWith("http")
|
||||||
|
? path
|
||||||
|
: baseUrl.TrimEnd('/') + path;
|
||||||
|
return Redirect(target);
|
||||||
|
}
|
||||||
|
|
||||||
// ── Listing ───────────────────────────────────────────────────────────────────
|
// ── Listing ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// <summary>GET /v1/payments — list the caller's payment history</summary>
|
/// <summary>GET /v1/payments — list the caller's payment history</summary>
|
||||||
@@ -62,7 +74,7 @@ public class PaymentsController(IPaymentService paymentService) : ControllerBase
|
|||||||
[FromQuery] string Status)
|
[FromQuery] string Status)
|
||||||
{
|
{
|
||||||
var frontendUrl = await paymentService.HandleZarinPalCallbackAsync(Authority, Status);
|
var frontendUrl = await paymentService.HandleZarinPalCallbackAsync(Authority, Status);
|
||||||
return Redirect(frontendUrl);
|
return RedirectFrontend(frontendUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── FlatRender Pay broker flow ────────────────────────────────────────────────
|
// ── FlatRender Pay broker flow ────────────────────────────────────────────────
|
||||||
@@ -80,7 +92,7 @@ public class PaymentsController(IPaymentService paymentService) : ControllerBase
|
|||||||
[FromQuery] string? id)
|
[FromQuery] string? id)
|
||||||
{
|
{
|
||||||
var frontendUrl = await paymentService.HandleBrokerCallbackAsync(payment_id, id ?? "");
|
var frontendUrl = await paymentService.HandleBrokerCallbackAsync(payment_id, id ?? "");
|
||||||
return Redirect(frontendUrl);
|
return RedirectFrontend(frontendUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── SnapPay flow ──────────────────────────────────────────────────────────────
|
// ── SnapPay flow ──────────────────────────────────────────────────────────────
|
||||||
@@ -109,7 +121,7 @@ public class PaymentsController(IPaymentService paymentService) : ControllerBase
|
|||||||
[FromQuery] string shapSnapStatus)
|
[FromQuery] string shapSnapStatus)
|
||||||
{
|
{
|
||||||
var frontendUrl = await paymentService.HandleSnapPayCallbackAsync(paymentToken, shapSnapStatus);
|
var frontendUrl = await paymentService.HandleSnapPayCallbackAsync(paymentToken, shapSnapStatus);
|
||||||
return Redirect(frontendUrl);
|
return RedirectFrontend(frontendUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Tara flow ─────────────────────────────────────────────────────────────────
|
// ── Tara flow ─────────────────────────────────────────────────────────────────
|
||||||
@@ -138,7 +150,7 @@ public class PaymentsController(IPaymentService paymentService) : ControllerBase
|
|||||||
[FromQuery] string status)
|
[FromQuery] string status)
|
||||||
{
|
{
|
||||||
var frontendUrl = await paymentService.HandleTaraCallbackAsync(token, status);
|
var frontendUrl = await paymentService.HandleTaraCallbackAsync(token, status);
|
||||||
return Redirect(frontendUrl);
|
return RedirectFrontend(frontendUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Stripe webhook ────────────────────────────────────────────────────────────
|
// ── Stripe webhook ────────────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Payment result landing. Payment gateways/brokers redirect the browser here after
|
||||||
|
* a purchase: /payment/result?status=success|failed&ref=<receipt>&gateway=<name>.
|
||||||
|
* Coins/plans are activated server-side; this page just reports the outcome.
|
||||||
|
*/
|
||||||
|
export default function PaymentResultPage({
|
||||||
|
params,
|
||||||
|
searchParams,
|
||||||
|
}: {
|
||||||
|
params: { locale: string };
|
||||||
|
searchParams: { status?: string; ref?: string; gateway?: string };
|
||||||
|
}) {
|
||||||
|
const fa = params.locale !== "en";
|
||||||
|
const success = searchParams.status === "success" || searchParams.status === "Paid";
|
||||||
|
const ref = typeof searchParams.ref === "string" ? searchParams.ref : "";
|
||||||
|
|
||||||
|
const t = {
|
||||||
|
successTitle: fa ? "پرداخت موفق بود" : "Payment successful",
|
||||||
|
successBody: fa
|
||||||
|
? "پرداخت شما با موفقیت انجام شد و اشتراکتان فعال گردید."
|
||||||
|
: "Your payment went through and your plan is now active.",
|
||||||
|
failTitle: fa ? "پرداخت ناموفق بود" : "Payment failed",
|
||||||
|
failBody: fa
|
||||||
|
? "پرداخت انجام نشد یا لغو شد. اگر مبلغی کسر شده باشد، طی ۷۲ ساعت بازگردانده میشود."
|
||||||
|
: "The payment didn't complete or was cancelled. Any charged amount is refunded within 72h.",
|
||||||
|
refLabel: fa ? "کد پیگیری" : "Tracking code",
|
||||||
|
toDashboard: fa ? "رفتن به داشبورد" : "Go to dashboard",
|
||||||
|
retry: fa ? "تلاش دوباره" : "Try again",
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex min-h-[70vh] items-center justify-center px-4 py-16">
|
||||||
|
<div className="w-full max-w-md rounded-2xl border border-gray-200 bg-white p-8 text-center shadow-sm">
|
||||||
|
<div
|
||||||
|
className={`mx-auto flex h-16 w-16 items-center justify-center rounded-full ${
|
||||||
|
success ? "bg-emerald-50 text-emerald-600" : "bg-red-50 text-red-600"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{success ? (
|
||||||
|
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
|
||||||
|
<path d="M20 6 9 17l-5-5" />
|
||||||
|
</svg>
|
||||||
|
) : (
|
||||||
|
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
|
||||||
|
<path d="M18 6 6 18M6 6l12 12" />
|
||||||
|
</svg>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h1 className="mt-5 text-xl font-bold text-gray-900">
|
||||||
|
{success ? t.successTitle : t.failTitle}
|
||||||
|
</h1>
|
||||||
|
<p className="mt-2 text-sm leading-relaxed text-gray-500">
|
||||||
|
{success ? t.successBody : t.failBody}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{success && ref ? (
|
||||||
|
<p className="mt-4 rounded-lg bg-gray-50 px-3 py-2 text-sm text-gray-600">
|
||||||
|
{t.refLabel}: <span className="font-mono font-medium text-gray-900" dir="ltr">{ref}</span>
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<div className="mt-6 flex justify-center gap-3">
|
||||||
|
<Link
|
||||||
|
href="/dashboard"
|
||||||
|
className="rounded-lg bg-primary-600 px-5 py-2.5 text-sm font-medium text-white hover:bg-primary-500"
|
||||||
|
>
|
||||||
|
{t.toDashboard}
|
||||||
|
</Link>
|
||||||
|
{!success ? (
|
||||||
|
<Link
|
||||||
|
href="/pricing"
|
||||||
|
className="rounded-lg border border-gray-300 px-5 py-2.5 text-sm font-medium text-gray-700 hover:border-gray-400"
|
||||||
|
>
|
||||||
|
{t.retry}
|
||||||
|
</Link>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user