Interface IPushNotificationAPI

All Superinterfaces:
IPushNotificationCommonAPI
All Known Implementing Classes:
ClientSessionPushAPI

public interface IPushNotificationAPI extends IPushNotificationCommonAPI
The Push Notification (PN) API of one client session: the device the user is on right now.

Obtained from IClientSessionGyro.getPushNotificationAPI(). Everything in IPushNotificationCommonAPI applies, in particular the threading rule: every callback runs on this client session's worker thread.

Which calls run on the client. queryDeviceState(BiFunction), subscribe(String, BiFunction), unsubscribe(String, BiFunction) and resetDevice(BiFunction) need the device itself - its notification permission, its Service Worker or its FirebaseX plugin - so they go out to the client through the channel, PushChannelProps under $env (ruled 2026-09-24, RR-1 B), and complete when the client answers. sendToClient(IPushNotification, String, PushSendOptions, BiFunction) executes on the server, but it targets the device by the UUID the channel reports, so it needs the channel too (RR-16 A). The channel exists since P4-06 (PUSH-PROPS-DESIGN section 3): the client creates it under $env when the session is created, the server writes the application's VAPID public key into it, and since P4-10 the in-session tier delivers down it and sendToClient(IPushNotification, String, PushSendOptions, BiFunction) selects this session by it. queryDeviceState(BiFunction), sendToClient(IPushNotification, String, PushSendOptions, BiFunction), subscribe(String, BiFunction) (P4-07), unsubscribe(String, BiFunction), resetDevice(BiFunction) and resetDeviceEverywhere(BiFunction) (P4-08), and setSubscribeAction(IGProp, String, BiFunction) and removeSubscribeAction(IGProp) (P4-09, the click that asks the user) are live. In every session that has no channel - a Designer, Design Viewer or VSViewer session, a client built before P4 - each client-bound call fails with the reason PushNotificationException.NO_DEVICE, posted to the session's worker, never thrown (design 3.2). The engine-bound calls of IPushNotificationCommonAPI need no channel and work in every session.

The device is identified by the device UUID the client sends as X-iizi on the PN HTTP API, the single device identity of Push Notification (PUSH-WIRE-CONTRACT section 2.5); the application session's own device UUID plays no part. A client session that has not reported it cannot be targeted; its calls fail with the reason no_device.

Author:
Christopher Mindus
  • Method Summary

    Modifier and Type
    Method
    Description
    void
    Queries what this device can do: whether it supports Push Notification at all, which transport it would use, what the user has granted, and whether it is subscribed for this application.
    void
    Removes the subscribe action of a button configured with setSubscribeAction(IGProp, String, BiFunction): the next click on it subscribes nothing.
    void
    Removes every subscription of the session's user on this device for this application, every topic (ruled 2026-09-24, RR-15 B), on this server and then on the device as unsubscribe(String, BiFunction) does once no row remains: a Browser gives up the subscription it holds under this application's key, a Cordova device keeps its shared FCM token.
    void
    Removes every subscription of the session's user on this device, for every application and topic, iiziRun's own rows (*) included: the Java form of the HTTP reset with an empty body (PUSH-WIRE-CONTRACT section 5.4), what iiziRun's Reset does on the server (ruled 2026-09-25, P4-b).
    void
    Sends a notification to this device only: in-session over this client session when the delivery mode allows it and the client is connected, and through the device's subscription for this application and topic when it holds one - the device is the one the channel reported (RR-16 A), and the in-session target is this client session's channel.
    void
    Configures the subscribe action of a button (PUSH-PROPS-DESIGN section 8.2, ruled 2026-09-24 RR-6 B and RR-7 A): the click on component, on the client and inside the user's gesture, subscribes this device for this application and topic - the one place where Push Notification is not driven from the server (PUSH-WIRE-CONTRACT section 9.4a) - and this server then stores the subscription as this session's user, this application, this device and the topic, exactly as subscribe(String, BiFunction) does, with no privilege checked (RR-5 A), and calls callback with the outcome.
    void
    Subscribes this device for this application and a topic: creates the device's subscription with its Service Worker (a Browser) or asks FirebaseX for its token (Cordova), and stores it with this server as this session's user, this application, this device and the topic - the Java form of the HTTP subscribe (PUSH-WIRE-CONTRACT section 5.2), with no HTTP and no Push Notification privilege checked: the application decides, so a user refused subscribe on the HTTP API can still be subscribed by an application (ruled 2026-09-24, RR-5 A).
    void
    Removes this device's subscription for this application and a topic: on this server the row of the session's user, this application, the topic and this device - the Java form of the HTTP unsubscribe (PUSH-WIRE-CONTRACT section 5.3), no privilege checked - and then, on the device, the push subscription a Browser holds under this application's key, but only when no row of this application remains for this device: a Browser holds one subscription for every topic of an application (PUSH-PROPS-DESIGN section 4.2, 8.7), and one it holds under another key - the server's own, or another application's - is left alone.
  • Method Details

    • queryDeviceState

      void queryDeviceState(BiFunction<IPushDeviceState, Exception, Void> callback)
      Queries what this device can do: whether it supports Push Notification at all, which transport it would use, what the user has granted, and whether it is subscribed for this application.
      Parameters:
      callback - Called on the client session's worker thread with the state, or with the failure.
      Throws:
      NullPointerException - If callback is null.
    • subscribe

      void subscribe(String topic, BiFunction<IPushSubscribeResult, Exception, Void> callback)
      Subscribes this device for this application and a topic: creates the device's subscription with its Service Worker (a Browser) or asks FirebaseX for its token (Cordova), and stores it with this server as this session's user, this application, this device and the topic - the Java form of the HTTP subscribe (PUSH-WIRE-CONTRACT section 5.2), with no HTTP and no Push Notification privilege checked: the application decides, so a user refused subscribe on the HTTP API can still be subscribed by an application (ruled 2026-09-24, RR-5 A). Listeners of the user's sessions get onSubscriptionCreated for a new row.

      On a Browser this call can only succeed when the user has already granted notifications. Browsers show the permission prompt only in response to a genuine user gesture, and Safari refuses the subscription outright without one; a request sent from the server reaches the client in a WebSocket message handler, after any click has returned, and can never count as one. So with the permission undecided this call fails at once with gesture_required, without prompting (ruled 2026-09-24, RR-8 A); with it granted the client attempts the subscription, and a Browser that refuses for want of a gesture is reported as gesture_required too (RR-9 A). The first subscription of a Browser user must come from a click: configure the subscribe action with setSubscribeAction(IGProp, String, BiFunction) when the panel is created and handle the outcome in that action's callback. Calling this method from an action handler on the server does NOT count as the user's click - by the time the server runs, the click is over. gesture_required is therefore not an error in the application's code: it means "ask the user to press the button you configured". This is the one place where Push Notification is not driven from the server (PUSH-WIRE-CONTRACT section 9.4a). On Cordova there is no gesture: FirebaseX asks the system's permission when it is undecided (the system dialog on iOS and on Android 13 and later), and the call waits up to five minutes for the answer.

      What else the callback can report. permission_denied is final until the user changes it in the Browser's or the operating system's settings; no code can ask again. key_conflict: the Browser already holds a push subscription under another VAPID key - the server's own (iiziRun, *) or another application's - which is never replaced (RR-21 A); or a Cordova device's FCM token belongs to another Firebase project than this application's, as it does inside iiziRun Developer, where an application reaches the device through the in-session tier and iiziRun's * subscription only (ruled 2026-09-26, CC-PUSH-ROADMAP row 4 A). not_configured: this application offers no transport the device uses (no VAPID key pair for a Browser, no Firebase project for Cordova); IPushNotificationCommonAPI.queryAvailableTransports(BiFunction) tells beforehand. unsupported: the device cannot receive Push Notification at all - on iOS a web page receives it only as a Home Screen web app, and an ordinary Safari tab reports UNSUPPORTED. no_user: the session has no authenticated user to store the row for. timeout: the client did not answer within five minutes. bad_request: the topic is not a valid name, or the client answered with a subscription lacking its endpoint or keys, and nothing was stored. Every failure is a PushNotificationException: its reason is the code to branch on, and PushNotificationException.getUserText() a text the user can be shown for it - for permission_denied on a phone "Notifications are off for this app: turn them on in the phone's Settings." - since a phone whose user once refused is never asked again, and nothing else tells the user why nothing happens (CC-FIX-IIZIRUN-RESUME-RECONNECT, AP-D-2 (c)); an application with a text table of its own uses the reason as the key of its entry.

      The callback runs on the client session's worker. If the session is disposed first, it is not called and the outcome is logged at INFO.

      Parameters:
      topic - The topic, the empty string for the application-level subscription; a name is validated as the HTTP API validates one (PUSH-WIRE-CONTRACT section 2.4).
      callback - Called on the client session's worker thread with the result - whether a new row was stored, and the subscription as stored - or with the failure.
      Throws:
      NullPointerException - If topic or callback is null.
    • unsubscribe

      void unsubscribe(String topic, BiFunction<Integer, Exception, Void> callback)
      Removes this device's subscription for this application and a topic: on this server the row of the session's user, this application, the topic and this device - the Java form of the HTTP unsubscribe (PUSH-WIRE-CONTRACT section 5.3), no privilege checked - and then, on the device, the push subscription a Browser holds under this application's key, but only when no row of this application remains for this device: a Browser holds one subscription for every topic of an application (PUSH-PROPS-DESIGN section 4.2, 8.7), and one it holds under another key - the server's own, or another application's - is left alone. A Cordova device keeps its FCM token, which every application on the device shares, and no FCM Topic is unsubscribed (ruled 2026-09-26, R-5 A: the server's rows are the subscription). Listeners of the user's sessions get onSubscriptionRemoved with the reason unsubscribed for each row removed.

      The server's part comes first and stands whatever the device answers: a timeout reported here means the rows are gone and the device may still hold its subscription, which the next subscribe answers as it is. Zero removed is a success: the state the caller wanted already holds. no_user when the session has no authenticated user; bad_request for a topic that is not a valid name.

      Parameters:
      topic - The topic, the empty string for the application-level subscription; a name is validated as the HTTP API validates one.
      callback - Called on the client session's worker thread with the number of subscriptions removed from the server, zero when there was none, or with the failure.
      Throws:
      NullPointerException - If topic or callback is null.
    • resetDevice

      void resetDevice(BiFunction<Integer, Exception, Void> callback)
      Removes every subscription of the session's user on this device for this application, every topic (ruled 2026-09-24, RR-15 B), on this server and then on the device as unsubscribe(String, BiFunction) does once no row remains: a Browser gives up the subscription it holds under this application's key, a Cordova device keeps its shared FCM token. Other applications' rows on this device, and iiziRun's own (*), survive; listeners of the user's sessions get onSubscriptionRemoved with the reason reset for each row removed. Removing every application's rows is resetDeviceEverywhere(BiFunction), the HTTP reset with an empty body (PUSH-WIRE-CONTRACT section 5.4), and iiziRun's Reset.

      The server's part comes first and stands whatever the device answers; zero removed is a success; no_user when the session has no authenticated user.

      Parameters:
      callback - Called on the client session's worker thread with the number of subscriptions removed from the server, or with the failure.
      Throws:
      NullPointerException - If callback is null.
    • resetDeviceEverywhere

      void resetDeviceEverywhere(BiFunction<Integer, Exception, Void> callback)
      Removes every subscription of the session's user on this device, for every application and topic, iiziRun's own rows (*) included: the Java form of the HTTP reset with an empty body (PUSH-WIRE-CONTRACT section 5.4), what iiziRun's Reset does on the server (ruled 2026-09-25, P4-b). The one member of this API that reaches beyond this application's subscriptions. On the device a Browser then gives up its push subscription whatever key it was made under - nothing of this user on this device remains to deliver to - and a Cordova device keeps its FCM token, which is never unregistered by the Java API. Listeners of the user's sessions get onSubscriptionRemoved with the reason reset for each row of their own application.

      The server's part comes first and stands whatever the device answers; zero removed is a success; no_user when the session has no authenticated user. Stopping this application's notifications alone is resetDevice(BiFunction).

      Parameters:
      callback - Called on the client session's worker thread with the number of subscriptions removed from the server, or with the failure.
      Throws:
      NullPointerException - If callback is null.
    • sendToClient

      void sendToClient(IPushNotification notification, String topic, PushSendOptions options, BiFunction<IPushSendResult, Exception, Void> callback)
      Sends a notification to this device only: in-session over this client session when the delivery mode allows it and the client is connected, and through the device's subscription for this application and topic when it holds one - the device is the one the channel reported (RR-16 A), and the in-session target is this client session's channel. Under the default mode, DeliveryMode.BOTH since P4-10 (ruled 2026-09-24, RR-14b B), both happen: the result then counts two deliveries for one device, and on a Browser the two arrive as ONE notification, shown under the same tag (PUSH-PROPS-DESIGN section 9.1). DeliveryMode.PREFER_IN_SESSION in the options keeps it to the session while the client is connected, DeliveryMode.REQUIRE_DEVICE to the subscription.

      Works in a session without an authenticated user (ruled 2026-09-26, CC-PUSH-ROADMAP row 3 B): the call names this session, not a user, so the in-session delivery reaches the page while the device tier matches no stored subscription - a subscription belongs to a user - and the outcome's user ID is -1 with an empty user name. The fan-out sends of IAppSessionPushNotificationAPI stay user-bound and fail no_user there. A topic named here does not gate the in-session delivery as it gates a fan-out (row 2 B): the device is named, so the session is the target.

      Parameters:
      notification - The notification, built with IPushNotificationMessageBuilder.
      topic - The topic, the empty string for the application-level subscription.
      options - The time-to-live, urgency and delivery mode, or null for every default.
      callback - Called on the client session's worker thread with the result, whose target outcomes name this device, or with the failure.
      Throws:
      NullPointerException - If notification, topic or callback is null.
    • setSubscribeAction

      void setSubscribeAction(IGProp<?> component, String topic, BiFunction<IPushSubscribeResult, Exception, Void> callback)
      Configures the subscribe action of a button (PUSH-PROPS-DESIGN section 8.2, ruled 2026-09-24 RR-6 B and RR-7 A): the click on component, on the client and inside the user's gesture, subscribes this device for this application and topic - the one place where Push Notification is not driven from the server (PUSH-WIRE-CONTRACT section 9.4a) - and this server then stores the subscription as this session's user, this application, this device and the topic, exactly as subscribe(String, BiFunction) does, with no privilege checked (RR-5 A), and calls callback with the outcome. On a Browser the click is what shows the permission prompt: pushManager.subscribe() is called synchronously inside the click, as measured on 2026-09-26 (P4-01) - Firefox, Safari and an iPadOS Home Screen web app show the prompt only there and refuse without a gesture, Chrome prompts either way. On Cordova FirebaseX asks the system's permission when it is undecided, and no gesture is needed.

      A session without an authenticated user answers no_user before any prompt (CC-FIX-PWA-FOLLOWUPS, PWA-D-6): the row a click stores is the user's, so the client refuses the click at once from the channel's auth and no permission is spent on it. Configure the action all the same: the first click after the sign-in asks and subscribes.

      The callback is called once per click, not once per configuration, and may be called several times in one session: the user may click, refuse, and click again. It runs on the client session's worker with the result - whether a new row was stored, and the subscription as stored - or with the failure: permission_denied (the prompt's Block, or a permission already denied: final until the user changes it in the Browser's or the operating system's settings, no code can ask again), dismissed (the prompt closed without an answer: the permission stays undecided and the next click asks again), key_conflict (the Browser already holds a push subscription under another VAPID key, never replaced, RR-21 A; or a Cordova device's FCM token belongs to another Firebase project, CC-PUSH-ROADMAP row 4 A), not_configured (this application offers no transport the device uses), unsupported (the device cannot receive Push Notification at all: on iOS a web page receives it only as a Home Screen web app whose manifest says "display": "standalone", and an ordinary Safari tab reports it), no_user (the session has no authenticated user to store the row for), bad_request (the client answered with a subscription lacking its endpoint or keys, and nothing was stored). gesture_required is not expected from a click and is reported if a Browser refuses inside one. Every failure is a PushNotificationException whose PushNotificationException.getUserText() is a text the user can be shown for it (see subscribe(String, BiFunction)).

      The component's own action handler (@OnUIAction) still runs, at once, before the user has answered the prompt (RR-20 A): do not decide anything about the subscription there. The client disables the button while the outcome is pending and enables it again after; every other follow-up - a message, hiding the button, changing the screen - is this callback's (RR-6 B). Setting an action on a component that has one replaces it. A successful click stores the subscription even if you removed the action while the prompt was open: the device holds it, and a row nobody stores is a device the server cannot reach; the callback is then not called and the outcome is logged at INFO.

      Configure it when the panel is created, and show the button only where it can work (queryDeviceState(BiFunction)). The application's VAPID public key is refreshed into the client when the action is configured, so a key rotated since the session was created reaches the click. With no channel - a Designer, Design Viewer or VSViewer session - nothing is configured and no_device is posted to the callback once; an invalid topic posts bad_request once.

      Parameters:
      component - The button: a UIButton of this client session's screen (P4 hooks the click of UIButton only), named to the client by its property path (RR-7 A).
      topic - The topic, the empty string for the application-level subscription; a name is validated as the HTTP API validates one (PUSH-WIRE-CONTRACT section 2.4).
      callback - Called on the client session's worker thread, once per click, with the result or with the failure.
      Throws:
      NullPointerException - If component, topic or callback is null.
      IllegalArgumentException - If component is not a UIButton, or not in this client session's property tree.
    • removeSubscribeAction

      void removeSubscribeAction(IGProp<?> component)
      Removes the subscribe action of a button configured with setSubscribeAction(IGProp, String, BiFunction): the next click on it subscribes nothing. A no-op on a component that has none, and in a session without a channel. A click whose prompt was open when the action was removed still stores the subscription the user allowed (see setSubscribeAction(IGProp, String, BiFunction)).
      Parameters:
      component - The button.
      Throws:
      NullPointerException - If component is null.
      IllegalArgumentException - If component is not a UIButton.