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>
134 lines
6.7 KiB
Markdown
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.
|