`code` was ambiguous next to Python's own use of "code"; `action_code` names its actual role (a click-action lookup key). click_object_id_value pairs it consistently with action_code. Renamed on both PushMessage and NotificationLink via a data-preserving RenameField migration, threaded through the serializer, admin, bulk-push path, and docs. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
134 lines
6.8 KiB
Markdown
134 lines
6.8 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 an **action_code**, and the notifications service
|
|
resolves that action_code against an admin-managed table (`NotificationLink`:
|
|
`action_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
|
|
action_code below with your actual URL format, and every notification of
|
|
that type immediately starts carrying it. If an action_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 action_code, ask whoever owns that
|
|
panel to update it.
|
|
|
|
## What's clickable today, by action_code
|
|
|
|
All of these are live in the `advertising` service:
|
|
|
|
### Billboards & content
|
|
|
|
| Action 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 action_code above sends the ad's `uuid` as `click_object_id_value`, 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
|
|
|
|
| Action 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_value`. 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.
|