notifications/docs/frontend-notification-click-actions.md
Ali Asadi 354bbc26cf docs(notifications): add service overview, implementation guide, and frontend handoff doc
Three docs for three audiences: service-overview.md for anyone integrating
with or operating the service, implementation.md for engineers maintaining
this codebase, and frontend-notification-click-actions.md as a self-contained
handoff for the client team to start building tap-to-navigate against — it
flags the click_url URL format as an unconfirmed placeholder needing their
input, and catalogs every notification type currently sending one.

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

6.1 KiB

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:

{
  "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.

⚠️ The URL format below is a placeholder — needs your input

Every click_url currently being sent is built from a setting called FRONTEND_BASE_URL, defaulting to https://app.gooyal.ir, with a path like /ads/<uuid> or /escrow/<uuid> appended. Nobody has confirmed this matches your actual app's routing — whether you use a custom URI scheme (gooyal://ads/<uuid>), a universal/app link (https://...), or something else entirely (a route name + params instead of a URL at all).

This was built centrally on purpose so it's a one-line change once you tell us: every click_url in the system is built by one helper function per producer service (utils/deep_links.py in advertising), not scattered across dozens of call sites. Please confirm your scheme and we'll update it — nothing about the client-side contract above changes either way, only the string inside click.url.

What's clickable today

All of the following are live in the advertising service and carry a real click_url pointing at the ad (/ads/<uuid>) or escrow deal (/escrow/<uuid>):

Billboards & content (→ /ads/<uuid>)

Event Notification text (fa)
Someone viewed your billboard شخصی شروع به مشاهده بیلبورد شما کرد.
Billboard created بیلبود شما با موفقیت ساخته شد.
Billboard approved بیلبورد شما تایید شد.
Billboard rejected بیلبود شما رد شد.
Someone commented on your billboard یک نفر برای بیلبورد شما نظر گذاشت!
Someone replied to your comment یک نفر به نظر شما پاسخ داد!
Someone bought your content درآمد جدید دارید! یک نفر محتوای شما را خرید.
Someone supported your content یک نفر از محتوای شما حمایت کرد!
Pin about to expire (time) زمان پین رو به اتمامه!
Pin expired (time) پین شما منقضی شد!
Pin credit about to run out (budget) اعتبار پین رو به اتمامه!
Pin credit exhausted (budget) اعتبار پین شما به پایان رسید!

Note the last four are two independent signals — a billboard can lapse because its display time ran out, or because its budget (balance vs. reward-per-view) ran out. Both currently point at the same ad detail page; if your UI wants to show a different call-to-action (extend time vs. top up credit) based on which one fired, that distinction is in the extras payload via which notification title/text arrived, not in the URL itself — ask if you need a structured signal here instead of parsing title text.

Escrow deals (→ /escrow/<uuid>)

The full P2P deal lifecycle — buyer pays, seller approves/rejects, delivery, confirm, dispute, payout/refund. All 11 possible state transitions notify whichever party (buyer or seller) needs to act or be informed next, each with a click_url to that deal's detail page. If you're building an escrow detail screen, treat every push in this domain as "go look at this deal" — the screen itself should reflect current state, not the specific notification that triggered the tap.

Not yet migrated to this mechanism

  • chat: sends extras = {"conversation_uuid": ..., "post_id": ...} directly, without a 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. If you'd rather chat also send a click_url once your routing scheme is confirmed, that's a small change on our side; let us know.
  • promotions: integrated with the notifications service but not currently sending any notification with real content (extras={} today) — nothing to build against yet.

Questions for you before we finalize

  1. What's your app's actual deep-link scheme — custom URI, universal link, or something else?
  2. Do you want the "time expiring" vs. "credit expiring" pin notifications distinguished by something other than title text?
  3. Do you want chat migrated onto click_url too, or is your existing conversation_uuid/post_id handling staying as-is?