notifications/docs/frontend-notification-click-actions.md
Ali Asadi d473f61790 docs(notifications): update docs for the code -> NotificationLink mechanism
Reflects the switch from hard-coded FRONTEND_BASE_URL/utils.deep_links to
admin-managed NotificationLink rows: implementation.md documents the
resolution logic and the new migration, frontend-notification-click-actions.md
replaces the "confirm your URL scheme" ask with the actual code catalog
(now that the scheme itself is an admin panel concern, not something the
frontend team needs to weigh in on for us to ship), and
notification-click-actions.md gets a pointer at the top so it reads as the
historical first pass it now is, not the current mechanism.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-22 15:23:45 +03:30

134 lines
6.7 KiB
Markdown

# Frontend integration guide: notification click actions
**Audience**: the team building the Gooyal client app(s) that receive Gotify
push notifications. This is everything you need to start building tap-to-navigate,
independent of the backend repos.
## The one thing to build
When a push notification is tapped, read a URL out of the notification's
payload and navigate to it. That's the entire client-side contract — one
handler, used by every notification type below, present or future.
## Where the URL lives
Gotify delivers a JSON payload per message. The URL — when the notification
is clickable at all — is under a reserved key, per
[Gotify's own convention](https://gotify.net/docs/pushmsg#extras):
```json
{
"title": "بیلبورد شما تایید شد.",
"message": "بیلبورد گربه (تست) تایید شد.",
"priority": 5,
"extras": {
"ads::ad::approve": true,
"ad_uuid": "8fb638c2-e6a4-4baf-aefe-83dab78fb5bd",
"client::notification": {
"click": { "url": "https://app.gooyal.ir/ads/8fb638c2-e6a4-4baf-aefe-83dab78fb5bd" }
}
}
}
```
Pull `extras["client::notification"]["click"]["url"]`. If it's present, tapping
navigates there. **If it's absent, do nothing on tap** — see next section.
## `click_url` is optional — most notifications are not clickable
Do not assume every notification carries a URL. The backend field this comes
from is explicitly optional (nullable end-to-end, not just "sometimes empty
string") specifically because not every notification type has a sensible
destination. Any notification without a `client::notification.click.url` key
should render and behave exactly as a non-interactive notification — no
error, no dead tap target, just no navigation.
You may also see other keys under `extras` alongside (or instead of)
`client::notification` — e.g. chat currently sends `conversation_uuid` and
`post_id` directly, not yet through this mechanism (see "Not yet migrated"
below). Ignore keys you don't recognize; don't treat an unfamiliar key as an
error.
## URLs are admin-configured, not hard-coded — nothing for you to build here
The actual destination string is no longer computed in backend code. Each
notification type has a **code**, and the notifications service resolves
that code against an admin-managed table (`NotificationLink`: `code`,
`url_template`, `is_active`) at send time. `url_template` supports a
`{object_id}` placeholder, e.g. `https://app.gooyal.ir/ads/{object_id}` or
`gooyal://ads/{object_id}` — whichever scheme your app actually uses.
This means: **your routing scheme is a config decision, not a code change**.
Whoever manages the notifications service's admin panel enters one row per
code below with your actual URL format, and every notification of that type
immediately starts carrying it. If a code has no row yet (or its row is
marked inactive), that notification simply has no `click_url` — same as any
other non-clickable notification, no error.
Practically, this doesn't change anything about what you build — you still
just read `extras["client::notification"]["click"]["url"]` and navigate if
it's present. It changes who's responsible for the URL *string itself*: not
a backend deploy, just an admin panel entry. If you want a different value
than what's currently configured for any code, ask whoever owns that panel
to update it.
## What's clickable today, by code
All of these are live in the `advertising` service:
### Billboards & content
| Code | Notification text (fa) |
|---|---|
| `billboard.viewed` | شخصی شروع به مشاهده بیلبورد شما کرد. |
| `billboard.created` | بیلبود شما با موفقیت ساخته شد. |
| `billboard.approved` | بیلبورد شما تایید شد. |
| `billboard.rejected` | بیلبود شما رد شد. |
| `billboard.commented` | یک نفر برای بیلبورد شما نظر گذاشت! |
| `billboard.replied` | یک نفر به نظر شما پاسخ داد! |
| `billboard.content_bought` | درآمد جدید دارید! یک نفر محتوای شما را خرید. |
| `billboard.content_supported` | یک نفر از محتوای شما حمایت کرد! |
| `billboard.pin_expiring` | زمان پین رو به اتمامه! |
| `billboard.pin_expired` | پین شما منقضی شد! |
| `billboard.credit_low` | اعتبار پین رو به اتمامه! |
| `billboard.credit_exhausted` | اعتبار پین شما به پایان رسید! |
Every code above sends the ad's `uuid` as `click_object_id`, so a
`url_template` of `https://app.gooyal.ir/ads/{object_id}` (or your app's
real equivalent) resolves correctly for all twelve. They're still separate
codes rather than one shared one, on purpose: `billboard.pin_expiring` and
`billboard.credit_low` are genuinely different situations (time running out
vs. budget running out) even though they'd point at the same screen today —
giving each its own code means that can diverge later (e.g. deep-linking
straight to an "extend time" vs. "top up credit" action) without any code
change, just a new admin row.
### Escrow deals
| Code | Trigger |
|---|---|
| `escrow.request_deal` | Buyer paid, seller needs to review |
| `escrow.cancel_deal` | Buyer canceled before seller responded |
| `escrow.reject_deal` | Seller declined the deal |
| `escrow.approve_deal` | Seller confirmed the deal |
| `escrow.cancel_deal_by_seller` | Seller canceled an active deal |
| `escrow.request_cancel` | Buyer requested cancellation |
| `escrow.approve_cancel` | Seller accepted the cancellation |
| `escrow.reject_cancel` | Seller declined the cancellation |
| `escrow.confirm_by_seller` | Seller marked it delivered |
| `escrow.confirm_by_buyer` | Buyer confirmed receipt — deal completed |
| `escrow.request_judge` | A dispute was opened |
| `escrow.cancel_judge` | Buyer withdrew their dispute |
| `escrow.approve_judge` | Dispute resolved for the buyer |
| `escrow.reject_judge` | Dispute resolved for the seller |
| `escrow.timeout` | Deal expired without a response |
All fifteen send the escrow deal's `uuid` as `click_object_id`. Same
one-code-per-situation reasoning as billboards — they all currently resolve
to the same escrow-detail destination, but that's an admin config choice,
not a hard-coded one, so it's free to diverge later.
## Not yet migrated to this mechanism
- **chat**: sends `extras = {"conversation_uuid": ..., "post_id": ...}` directly, without a code or `click_url`. If your client already has custom handling for these two keys (built before this mechanism existed), it keeps working — this doc doesn't change that. Let us know if you'd rather chat send a code too.
- **promotions**: integrated with the notifications service but not currently sending any notification with real content (`extras={}` today) — nothing to build against yet.