Add backend Chapa payment success HTML page.
Serve /payment/success and /api/v1/payments/chapa/success to verify tx_ref on redirect and activate subscriptions, and share the payment success template with ArifPay. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
ed743cf841
commit
d3bbd8c95a
|
|
@ -16,7 +16,7 @@ CHAPA_PUBLIC_KEY=CHAPUBK_TEST-xxxxxxxx
|
||||||
CHAPA_WEBHOOK_SECRET=your_webhook_secret_from_dashboard
|
CHAPA_WEBHOOK_SECRET=your_webhook_secret_from_dashboard
|
||||||
CHAPA_BASE_URL=https://api.chapa.co/v1
|
CHAPA_BASE_URL=https://api.chapa.co/v1
|
||||||
CHAPA_CALLBACK_URL=https://your-api.example.com/api/v1/payments/chapa/callback
|
CHAPA_CALLBACK_URL=https://your-api.example.com/api/v1/payments/chapa/callback
|
||||||
CHAPA_RETURN_URL=https://your-app.example.com/payment/success
|
CHAPA_RETURN_URL=https://your-api.example.com/payment/success
|
||||||
CHAPA_RECEIPT_URL=
|
CHAPA_RECEIPT_URL=
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -29,9 +29,9 @@ Configure the same webhook URL in the Chapa dashboard:
|
||||||
1. Learner calls `POST /api/v1/subscriptions/checkout` or `POST /api/v1/payments/subscribe`.
|
1. Learner calls `POST /api/v1/subscriptions/checkout` or `POST /api/v1/payments/subscribe`.
|
||||||
2. Backend creates a pending payment and calls Chapa `POST /transaction/initialize`.
|
2. Backend creates a pending payment and calls Chapa `POST /transaction/initialize`.
|
||||||
3. Client redirects the user to `payment_url` (`checkout_url` from Chapa).
|
3. Client redirects the user to `payment_url` (`checkout_url` from Chapa).
|
||||||
4. After payment, Chapa calls `callback_url` and sends a webhook.
|
4. After payment, Chapa redirects the learner to `return_url` (`/payment/success`) and calls `callback_url`.
|
||||||
5. Backend verifies via `GET /transaction/verify/{tx_ref}` and activates the subscription.
|
5. The success page and callback both verify via Chapa `GET /transaction/verify/{tx_ref}` and activate the subscription when successful.
|
||||||
6. Client may poll `GET /api/v1/payments/verify/{tx_ref}` (`session_id` path param is the `tx_ref`).
|
6. Chapa also sends a webhook; client may poll `GET /api/v1/payments/verify/{tx_ref}` (`session_id` path param is the `tx_ref`).
|
||||||
|
|
||||||
## API Endpoints
|
## API Endpoints
|
||||||
|
|
||||||
|
|
@ -41,7 +41,9 @@ Configure the same webhook URL in the Chapa dashboard:
|
||||||
| POST | `/api/v1/payments/subscribe` | Yes | Same as checkout |
|
| POST | `/api/v1/payments/subscribe` | Yes | Same as checkout |
|
||||||
| GET | `/api/v1/payments/verify/:session_id` | Yes | Verify by `tx_ref` |
|
| GET | `/api/v1/payments/verify/:session_id` | Yes | Verify by `tx_ref` |
|
||||||
| POST | `/api/v1/payments/webhook` | No | Chapa webhook (HMAC signature required) |
|
| POST | `/api/v1/payments/webhook` | No | Chapa webhook (HMAC signature required) |
|
||||||
| GET | `/api/v1/payments/chapa/callback` | No | Chapa redirect callback |
|
| GET | `/api/v1/payments/chapa/callback` | No | Chapa server callback (JSON) |
|
||||||
|
| GET | `/api/v1/payments/chapa/success` | No | Chapa learner success page (HTML) |
|
||||||
|
| GET | `/payment/success` | No | Same HTML success page (`CHAPA_RETURN_URL`) |
|
||||||
| GET | `/api/v1/payments/methods` | No | Supported Chapa methods |
|
| GET | `/api/v1/payments/methods` | No | Supported Chapa methods |
|
||||||
|
|
||||||
### Initiate payment request
|
### Initiate payment request
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,9 @@
|
||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"html/template"
|
|
||||||
|
|
||||||
"Yimaru-Backend/internal/domain"
|
"Yimaru-Backend/internal/domain"
|
||||||
"Yimaru-Backend/internal/services/chapa"
|
"Yimaru-Backend/internal/services/chapa"
|
||||||
|
|
@ -332,15 +330,7 @@ func (h *Handler) HandleArifpaySuccessPage(c *fiber.Ctx) error {
|
||||||
c.Query("nonce"),
|
c.Query("nonce"),
|
||||||
)
|
)
|
||||||
|
|
||||||
page := arifpaySuccessPageData{
|
page := defaultPaymentSuccessPage()
|
||||||
Title: "Subscription Payment Successful",
|
|
||||||
Headline: "Your Yimaru Academy payment was received",
|
|
||||||
Body: "Thank you for your payment. Your subscription is being activated and you can return to Yimaru Academy shortly.",
|
|
||||||
BadgeLabel: "Payment successful",
|
|
||||||
StatusLabel: "Activation in progress",
|
|
||||||
ActionLabel: "Continue learning",
|
|
||||||
ActionHref: "/",
|
|
||||||
}
|
|
||||||
|
|
||||||
if ref != "" {
|
if ref != "" {
|
||||||
payment, err := h.arifpaySvc.VerifyPayment(c.Context(), ref)
|
payment, err := h.arifpaySvc.VerifyPayment(c.Context(), ref)
|
||||||
|
|
@ -364,7 +354,7 @@ func (h *Handler) HandleArifpaySuccessPage(c *fiber.Ctx) error {
|
||||||
page.Helper = "Return to Yimaru Academy and refresh your subscription status if you do not see access immediately."
|
page.Helper = "Return to Yimaru Academy and refresh your subscription status if you do not see access immediately."
|
||||||
}
|
}
|
||||||
|
|
||||||
html, err := renderArifpaySuccessPage(page)
|
html, err := renderPaymentSuccessPage(page)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return c.Status(fiber.StatusInternalServerError).SendString("Failed to render success page")
|
return c.Status(fiber.StatusInternalServerError).SendString("Failed to render success page")
|
||||||
}
|
}
|
||||||
|
|
@ -585,95 +575,3 @@ func paymentToRes(p *domain.Payment) *paymentRes {
|
||||||
|
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
type arifpaySuccessPageData struct {
|
|
||||||
Title string
|
|
||||||
Headline string
|
|
||||||
Body string
|
|
||||||
Helper string
|
|
||||||
BadgeLabel string
|
|
||||||
StatusLabel string
|
|
||||||
Reference string
|
|
||||||
PlanName string
|
|
||||||
ActionLabel string
|
|
||||||
ActionHref string
|
|
||||||
}
|
|
||||||
|
|
||||||
func renderArifpaySuccessPage(data arifpaySuccessPageData) (string, error) {
|
|
||||||
const tpl = `<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
||||||
<title>{{.Title}}</title>
|
|
||||||
</head>
|
|
||||||
<body style="margin:0;background:#f4f6fb;font-family:Inter,Roboto,Helvetica,Arial,sans-serif;color:#333;">
|
|
||||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="min-height:100vh;background:#f4f6fb;padding:24px 16px;">
|
|
||||||
<tr>
|
|
||||||
<td align="center">
|
|
||||||
<table role="presentation" width="640" cellspacing="0" cellpadding="0" style="max-width:640px;width:100%;background:#ffffff;border-radius:18px;overflow:hidden;box-shadow:0 14px 40px rgba(157,42,131,0.12);">
|
|
||||||
<tr>
|
|
||||||
<td style="background:linear-gradient(135deg,#7b1f6e 0%,#9d2a83 55%,#c43a9a 100%);padding:32px 28px;text-align:center;">
|
|
||||||
<div style="display:inline-block;padding:8px 14px;border-radius:999px;background:rgba(255,255,255,0.15);color:#fff;font-size:12px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;">{{.BadgeLabel}}</div>
|
|
||||||
<h1 style="margin:18px 0 8px;color:#fff;font-size:30px;line-height:1.2;">Yimaru Academy</h1>
|
|
||||||
<p style="margin:0;color:rgba(255,255,255,0.88);font-size:16px;">{{.Headline}}</p>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td style="padding:32px 28px;">
|
|
||||||
<div style="margin:0 auto 24px;width:76px;height:76px;border-radius:50%;background:#eef9f2;border:1px solid #cfead9;text-align:center;line-height:76px;font-size:40px;color:#1f9d55;">✓</div>
|
|
||||||
<p style="margin:0 0 18px;font-size:16px;line-height:1.7;color:#555;">{{.Body}}</p>
|
|
||||||
{{if .Helper}}<p style="margin:0 0 22px;font-size:14px;line-height:1.7;color:#777;">{{.Helper}}</p>{{end}}
|
|
||||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="margin:0 0 26px;background:#f8f3f8;border:1px solid #eddced;border-radius:12px;">
|
|
||||||
<tr>
|
|
||||||
<td style="padding:18px 20px;">
|
|
||||||
<p style="margin:0 0 8px;font-size:12px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:#9d2a83;">Status</p>
|
|
||||||
<p style="margin:0;font-size:18px;font-weight:700;color:#333;">{{.StatusLabel}}</p>
|
|
||||||
{{if .PlanName}}<p style="margin:14px 0 0;font-size:14px;color:#555;"><strong>Plan:</strong> {{.PlanName}}</p>{{end}}
|
|
||||||
{{if .Reference}}<p style="margin:8px 0 0;font-size:14px;color:#555;word-break:break-word;"><strong>Reference:</strong> {{.Reference}}</p>{{end}}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
<div style="text-align:center;">
|
|
||||||
<a href="{{.ActionHref}}" style="display:inline-block;padding:14px 24px;border-radius:10px;background:#9d2a83;color:#fff;text-decoration:none;font-size:15px;font-weight:700;">{{.ActionLabel}}</a>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td style="padding:20px 28px;background:#fafafa;border-top:1px solid #eee;text-align:center;">
|
|
||||||
<p style="margin:0;font-size:12px;line-height:1.6;color:#8a8a8a;">Yimaru Academy subscription payments are verified securely before access is granted.</p>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</body>
|
|
||||||
</html>`
|
|
||||||
|
|
||||||
t, err := template.New("arifpay-success").Parse(tpl)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
var buf bytes.Buffer
|
|
||||||
if err := t.Execute(&buf, data); err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return buf.String(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func firstNonEmpty(values ...string) string {
|
|
||||||
for _, value := range values {
|
|
||||||
if value != "" {
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func derefString(value *string) string {
|
|
||||||
if value == nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return *value
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -99,6 +99,56 @@ func (h *Handler) HandleChapaCallback(c *fiber.Ctx) error {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HandleChapaSuccessPage godoc
|
||||||
|
// @Summary Chapa payment success page
|
||||||
|
// @Description Displays the Yimaru Academy success page after Chapa redirects the learner to return_url
|
||||||
|
// @Tags payments
|
||||||
|
// @Produce html
|
||||||
|
// @Param trx_ref query string false "Chapa transaction reference (tx_ref)"
|
||||||
|
// @Param tx_ref query string false "Chapa transaction reference"
|
||||||
|
// @Param ref_id query string false "Chapa reference ID"
|
||||||
|
// @Param status query string false "Payment status from Chapa redirect"
|
||||||
|
// @Success 200 {string} string "HTML success page"
|
||||||
|
// @Router /api/v1/payments/chapa/success [get]
|
||||||
|
// @Router /payment/success [get]
|
||||||
|
func (h *Handler) HandleChapaSuccessPage(c *fiber.Ctx) error {
|
||||||
|
txRef := firstNonEmpty(
|
||||||
|
c.Query("trx_ref"),
|
||||||
|
c.Query("tx_ref"),
|
||||||
|
)
|
||||||
|
|
||||||
|
page := defaultPaymentSuccessPage()
|
||||||
|
|
||||||
|
if txRef != "" {
|
||||||
|
payment, err := h.chapaSvc.VerifyPayment(c.Context(), txRef)
|
||||||
|
if err != nil {
|
||||||
|
h.logger.Warn("Failed to verify Chapa success redirect", "error", err, "tx_ref", txRef)
|
||||||
|
page.Body = "Thank you for your payment. We are confirming it with Chapa and will activate your subscription shortly."
|
||||||
|
page.Helper = "You can safely return to Yimaru Academy. If activation takes longer than expected, refresh the app in a moment."
|
||||||
|
page.Reference = txRef
|
||||||
|
} else {
|
||||||
|
page.Reference = txRef
|
||||||
|
page.PlanName = derefString(payment.PlanName)
|
||||||
|
if payment.Status == string(domain.PaymentStatusSuccess) {
|
||||||
|
page.StatusLabel = "Subscription active"
|
||||||
|
page.Body = "Your Yimaru Academy subscription is active. You now have access to your learning content."
|
||||||
|
} else {
|
||||||
|
page.Body = "Thank you for your payment. We received your success redirect and are finalizing subscription activation."
|
||||||
|
page.StatusLabel = "Processing confirmation"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
page.Helper = "Return to Yimaru Academy and refresh your subscription status if you do not see access immediately."
|
||||||
|
}
|
||||||
|
|
||||||
|
html, err := renderPaymentSuccessPage(page)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(fiber.StatusInternalServerError).SendString("Failed to render success page")
|
||||||
|
}
|
||||||
|
c.Type("html", "utf-8")
|
||||||
|
return c.SendString(html)
|
||||||
|
}
|
||||||
|
|
||||||
// GetChapaPaymentMethods godoc
|
// GetChapaPaymentMethods godoc
|
||||||
// @Summary Get Chapa payment methods
|
// @Summary Get Chapa payment methods
|
||||||
// @Description Returns payment methods available on Chapa checkout
|
// @Description Returns payment methods available on Chapa checkout
|
||||||
|
|
|
||||||
110
internal/web_server/handlers/payment_success_page.go
Normal file
110
internal/web_server/handlers/payment_success_page.go
Normal file
|
|
@ -0,0 +1,110 @@
|
||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"html/template"
|
||||||
|
)
|
||||||
|
|
||||||
|
type paymentSuccessPageData struct {
|
||||||
|
Title string
|
||||||
|
Headline string
|
||||||
|
Body string
|
||||||
|
Helper string
|
||||||
|
BadgeLabel string
|
||||||
|
StatusLabel string
|
||||||
|
Reference string
|
||||||
|
PlanName string
|
||||||
|
ActionLabel string
|
||||||
|
ActionHref string
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderPaymentSuccessPage(data paymentSuccessPageData) (string, error) {
|
||||||
|
const tpl = `<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
|
<title>{{.Title}}</title>
|
||||||
|
</head>
|
||||||
|
<body style="margin:0;background:#f4f6fb;font-family:Inter,Roboto,Helvetica,Arial,sans-serif;color:#333;">
|
||||||
|
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="min-height:100vh;background:#f4f6fb;padding:24px 16px;">
|
||||||
|
<tr>
|
||||||
|
<td align="center">
|
||||||
|
<table role="presentation" width="640" cellspacing="0" cellpadding="0" style="max-width:640px;width:100%;background:#ffffff;border-radius:18px;overflow:hidden;box-shadow:0 14px 40px rgba(157,42,131,0.12);">
|
||||||
|
<tr>
|
||||||
|
<td style="background:linear-gradient(135deg,#7b1f6e 0%,#9d2a83 55%,#c43a9a 100%);padding:32px 28px;text-align:center;">
|
||||||
|
<div style="display:inline-block;padding:8px 14px;border-radius:999px;background:rgba(255,255,255,0.15);color:#fff;font-size:12px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;">{{.BadgeLabel}}</div>
|
||||||
|
<h1 style="margin:18px 0 8px;color:#fff;font-size:30px;line-height:1.2;">Yimaru Academy</h1>
|
||||||
|
<p style="margin:0;color:rgba(255,255,255,0.88);font-size:16px;">{{.Headline}}</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding:32px 28px;">
|
||||||
|
<div style="margin:0 auto 24px;width:76px;height:76px;border-radius:50%;background:#eef9f2;border:1px solid #cfead9;text-align:center;line-height:76px;font-size:40px;color:#1f9d55;">✓</div>
|
||||||
|
<p style="margin:0 0 18px;font-size:16px;line-height:1.7;color:#555;">{{.Body}}</p>
|
||||||
|
{{if .Helper}}<p style="margin:0 0 22px;font-size:14px;line-height:1.7;color:#777;">{{.Helper}}</p>{{end}}
|
||||||
|
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="margin:0 0 26px;background:#f8f3f8;border:1px solid #eddced;border-radius:12px;">
|
||||||
|
<tr>
|
||||||
|
<td style="padding:18px 20px;">
|
||||||
|
<p style="margin:0 0 8px;font-size:12px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:#9d2a83;">Status</p>
|
||||||
|
<p style="margin:0;font-size:18px;font-weight:700;color:#333;">{{.StatusLabel}}</p>
|
||||||
|
{{if .PlanName}}<p style="margin:14px 0 0;font-size:14px;color:#555;"><strong>Plan:</strong> {{.PlanName}}</p>{{end}}
|
||||||
|
{{if .Reference}}<p style="margin:8px 0 0;font-size:14px;color:#555;word-break:break-word;"><strong>Reference:</strong> {{.Reference}}</p>{{end}}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<div style="text-align:center;">
|
||||||
|
<a href="{{.ActionHref}}" style="display:inline-block;padding:14px 24px;border-radius:10px;background:#9d2a83;color:#fff;text-decoration:none;font-size:15px;font-weight:700;">{{.ActionLabel}}</a>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding:20px 28px;background:#fafafa;border-top:1px solid #eee;text-align:center;">
|
||||||
|
<p style="margin:0;font-size:12px;line-height:1.6;color:#8a8a8a;">Yimaru Academy subscription payments are verified securely before access is granted.</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</body>
|
||||||
|
</html>`
|
||||||
|
|
||||||
|
t, err := template.New("payment-success").Parse(tpl)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
var buf bytes.Buffer
|
||||||
|
if err := t.Execute(&buf, data); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return buf.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func defaultPaymentSuccessPage() paymentSuccessPageData {
|
||||||
|
return paymentSuccessPageData{
|
||||||
|
Title: "Subscription Payment Successful",
|
||||||
|
Headline: "Your Yimaru Academy payment was received",
|
||||||
|
Body: "Thank you for your payment. Your subscription is being activated and you can return to Yimaru Academy shortly.",
|
||||||
|
BadgeLabel: "Payment successful",
|
||||||
|
StatusLabel: "Activation in progress",
|
||||||
|
ActionLabel: "Continue learning",
|
||||||
|
ActionHref: "/",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func firstNonEmpty(values ...string) string {
|
||||||
|
for _, value := range values {
|
||||||
|
if value != "" {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func derefString(value *string) string {
|
||||||
|
if value == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return *value
|
||||||
|
}
|
||||||
|
|
@ -274,7 +274,9 @@ func (a *App) initAppRoutes() {
|
||||||
groupV1.Post("/payments/:id/cancel", a.authMiddleware, a.RequirePermission("payments.cancel"), h.CancelPayment)
|
groupV1.Post("/payments/:id/cancel", a.authMiddleware, a.RequirePermission("payments.cancel"), h.CancelPayment)
|
||||||
groupV1.Post("/payments/webhook", h.HandleChapaWebhook)
|
groupV1.Post("/payments/webhook", h.HandleChapaWebhook)
|
||||||
groupV1.Get("/payments/arifpay/success", h.HandleArifpaySuccessPage)
|
groupV1.Get("/payments/arifpay/success", h.HandleArifpaySuccessPage)
|
||||||
|
groupV1.Get("/payments/chapa/success", h.HandleChapaSuccessPage)
|
||||||
groupV1.Get("/payments/chapa/callback", h.HandleChapaCallback)
|
groupV1.Get("/payments/chapa/callback", h.HandleChapaCallback)
|
||||||
|
a.fiber.Get("/payment/success", h.HandleChapaSuccessPage)
|
||||||
|
|
||||||
// Direct Payments
|
// Direct Payments
|
||||||
groupV1.Post("/payments/direct", a.authMiddleware, a.RequirePermission("payments.direct_initiate"), h.InitiateDirectPayment)
|
groupV1.Post("/payments/direct", a.authMiddleware, a.RequirePermission("payments.direct_initiate"), h.InitiateDirectPayment)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user