From 1d43bf4aa5e735d1f60af1bd63e5f1df898da3e8 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Mon, 27 Apr 2026 15:05:47 +0100 Subject: [PATCH] Add push noty script and docs --- scripts/push-notification/README.md | 97 +++++++++++++++++++ .../payloads/chat-message.apns | 15 +++ .../payloads/chat-reaction.apns | 15 +++ .../push-notification/payloads/follow.apns | 14 +++ scripts/push-notification/payloads/like.apns | 14 +++ scripts/push-notification/payloads/reply.apns | 14 +++ scripts/push-notification/send.sh | 78 +++++++++++++++ 7 files changed, 247 insertions(+) create mode 100644 scripts/push-notification/README.md create mode 100644 scripts/push-notification/payloads/chat-message.apns create mode 100644 scripts/push-notification/payloads/chat-reaction.apns create mode 100644 scripts/push-notification/payloads/follow.apns create mode 100644 scripts/push-notification/payloads/like.apns create mode 100644 scripts/push-notification/payloads/reply.apns create mode 100755 scripts/push-notification/send.sh diff --git a/scripts/push-notification/README.md b/scripts/push-notification/README.md new file mode 100644 index 0000000000..d54c5e9294 --- /dev/null +++ b/scripts/push-notification/README.md @@ -0,0 +1,97 @@ +# push-notification + +Sends sample APNS payloads to a booted iOS simulator via `xcrun simctl push`. +Useful for exercising `useNotificationsHandler` (`src/lib/hooks/useNotificationHandler.ts`) +without a real APNS round-trip — foreground display behavior, tap responses, +and notification-driven navigation. + +## What this does and doesn't cover + +**Covers** — anything in `useNotificationsHandler`: + +- `setNotificationHandler` foreground behavior (banner, list, badge, sound flags) +- `addNotificationResponseReceivedListener` tap handling +- Account-switch flow when `recipientDid` differs from the signed-in account +- Navigation routing for each `reason` (post threads, profiles, conversations) + +**Does not cover** — `BlueskyNSE` (the iOS Notification Service Extension): + +- Communication Notification styling for chat messages +- Badge increment via `mutateWithBadge` +- Custom `dm.aiff` sound for chat messages + +The simulator does not reliably invoke NSEs for `simctl push` on recent iOS +versions (verified bypassed on iOS 26.4). To test NSE behavior, run on a real +device with a real APNS push, or unit-test `NotificationService.didReceive` +directly in Xcode. + +## Setup + +1. Boot an iOS simulator and install the app: + ``` + yarn ios + ``` +2. Sign in to the account you'll be testing against. The `recipientDid` in + each payload is substituted at send time and must match the signed-in DID, + otherwise: + - Chat notifications trigger the account-switch flow + - Other reasons are silently dropped by the handler +3. Find your DID. Easiest: visit your profile in a web browser and copy it + from the URL, or grep dev logs for `currentAccount`. + +## Usage + +``` +./send.sh [--did ] [--device ] [--bundle ] +``` + +Pass the DID once via env var to avoid repeating it: + +``` +export BLUESKY_TEST_DID=did:plc:yourdidhere + +./send.sh like +./send.sh chat-message +./send.sh follow +``` + +Defaults: `--device booted`, `--bundle xyz.blueskyweb.app`. Run `./send.sh --help` +for the full list of available payloads. + +## Foreground vs background + +`useNotificationsHandler` behaves differently depending on app state: + +- **Foreground** — `setNotificationHandler.handleNotification` decides whether + to show a banner, play a sound, etc. For chat reasons, the banner is + suppressed if `payload.convoId === currentConvoId` (you're already viewing + that conversation). +- **Background or tapped** — `addNotificationResponseReceivedListener` fires + on tap and runs the navigation routing in `notificationToURL`. + +To test the response listener, background the app first (`cmd+shift+H` in the +sim), send the push, then tap the banner. + +## Available payloads + +| Payload | Reason | Navigation target | +|---|---|---| +| `like.apns` | `like` | post thread (from `subject`) | +| `reply.apns` | `reply` | post thread (from `uri`) | +| `follow.apns` | `follow` | sender's profile (from `uri.host`) | +| `chat-message.apns` | `chat-message` | `MessagesConversation` with `convoId` | +| `chat-reaction.apns` | `chat-reaction` | `MessagesConversation` with `convoId` | + +The `subject` AT URIs reference fake post rkeys, so the destination screens +will fail to load real content — that's expected. Routing exercises the +navigation path, not the data fetch. + +## Adding a new payload + +1. Copy an existing `.apns` file in `payloads/` whose shape matches. +2. Set `aps.mutable-content: 1` and a real `aps.alert.{title,body}`. +3. Match the payload shape to `NotificationPayload` in + `src/lib/hooks/useNotificationHandler.ts` for the `reason` you're testing. +4. Use `__RECIPIENT_DID__` as the placeholder for the recipient — `send.sh` + substitutes it at send time. Use it anywhere a DID needs to belong to the + logged-in user (typically `recipientDid`, and post `subject` for likes/replies). diff --git a/scripts/push-notification/payloads/chat-message.apns b/scripts/push-notification/payloads/chat-message.apns new file mode 100644 index 0000000000..f2e576101b --- /dev/null +++ b/scripts/push-notification/payloads/chat-message.apns @@ -0,0 +1,15 @@ +{ + "aps": { + "alert": { + "title": "Ian", + "body": "Beep beep mfer" + }, + "mutable-content": 1 + }, + "reason": "chat-message", + "convoId": "3mkb6y3tjzu2r", + "messageId": "3mkdjhe6sd227", + "recipientDid": "did:plc:3jpt2mvvsumj2r7eqk4gzzjz", + "senderDisplayName": "Ian", + "senderHandle": "iwsmith.bsky.social" +} diff --git a/scripts/push-notification/payloads/chat-reaction.apns b/scripts/push-notification/payloads/chat-reaction.apns new file mode 100644 index 0000000000..c48966bd3c --- /dev/null +++ b/scripts/push-notification/payloads/chat-reaction.apns @@ -0,0 +1,15 @@ +{ + "aps": { + "alert": { + "title": "Alice Test", + "body": "reacted to your message" + }, + "mutable-content": 1 + }, + "reason": "chat-reaction", + "convoId": "3kfakeconvo0001", + "messageId": "3kfakemsg0001", + "recipientDid": "__RECIPIENT_DID__", + "senderDisplayName": "Alice Test", + "senderHandle": "alice.test" +} diff --git a/scripts/push-notification/payloads/follow.apns b/scripts/push-notification/payloads/follow.apns new file mode 100644 index 0000000000..1ff43bf250 --- /dev/null +++ b/scripts/push-notification/payloads/follow.apns @@ -0,0 +1,14 @@ +{ + "aps": { + "alert": { + "title": "alice.test", + "body": "followed you" + }, + "mutable-content": 1, + "sound": "default" + }, + "reason": "follow", + "uri": "at://did:plc:senderdummy00000000000000/app.bsky.graph.follow/3kfakefollow001", + "subject": "at://__RECIPIENT_DID__", + "recipientDid": "__RECIPIENT_DID__" +} diff --git a/scripts/push-notification/payloads/like.apns b/scripts/push-notification/payloads/like.apns new file mode 100644 index 0000000000..e16297f6b2 --- /dev/null +++ b/scripts/push-notification/payloads/like.apns @@ -0,0 +1,14 @@ +{ + "aps": { + "alert": { + "title": "alice.test", + "body": "liked your post" + }, + "mutable-content": 1, + "sound": "default" + }, + "reason": "like", + "uri": "at://did:plc:senderdummy00000000000000/app.bsky.feed.like/3kfakelike0001", + "subject": "at://__RECIPIENT_DID__/app.bsky.feed.post/3kfakepost0001", + "recipientDid": "__RECIPIENT_DID__" +} diff --git a/scripts/push-notification/payloads/reply.apns b/scripts/push-notification/payloads/reply.apns new file mode 100644 index 0000000000..34accf0f68 --- /dev/null +++ b/scripts/push-notification/payloads/reply.apns @@ -0,0 +1,14 @@ +{ + "aps": { + "alert": { + "title": "alice.test", + "body": "replied to your post" + }, + "mutable-content": 1, + "sound": "default" + }, + "reason": "reply", + "uri": "at://did:plc:senderdummy00000000000000/app.bsky.feed.post/3kfakereply0001", + "subject": "at://__RECIPIENT_DID__/app.bsky.feed.post/3kfakepost0001", + "recipientDid": "__RECIPIENT_DID__" +} diff --git a/scripts/push-notification/send.sh b/scripts/push-notification/send.sh new file mode 100755 index 0000000000..1d5450bfae --- /dev/null +++ b/scripts/push-notification/send.sh @@ -0,0 +1,78 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Sends a sample APNS payload to a booted iOS simulator. Useful for testing +# BlueskyNSE and useNotificationsHandler without a real APNS round-trip. +# +# Usage: +# scripts/push-test/send.sh [--did ] [--device ] [--bundle ] +# +# Examples: +# scripts/push-test/send.sh like --did did:plc:abc123 +# BLUESKY_TEST_DID=did:plc:abc123 scripts/push-test/send.sh chat-message + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PAYLOAD_DIR="$SCRIPT_DIR/payloads" + +device="booted" +bundle="xyz.blueskyweb.app" +did="${BLUESKY_TEST_DID:-}" +name="" + +while [[ $# -gt 0 ]]; do + case "$1" in + --did) did="$2"; shift 2;; + --device) device="$2"; shift 2;; + --bundle) bundle="$2"; shift 2;; + -h|--help) + cat < [--did ] [--device ] [--bundle ] + +Available payloads: +$(ls "$PAYLOAD_DIR" 2>/dev/null | sed 's/\.apns$//' | sed 's/^/ /') + +Pass --did or set BLUESKY_TEST_DID to substitute the recipient DID. The DID +must match the account currently signed in to the app, otherwise chat +notifications will trigger an account-switch flow and other reasons will be +silently dropped by the handler. +EOF + exit 0 + ;; + *) + if [[ -z "$name" ]]; then + name="$1" + else + echo "Unknown arg: $1" >&2 + exit 1 + fi + shift + ;; + esac +done + +if [[ -z "$name" ]]; then + echo "Error: payload name required. Run with --help for options." >&2 + exit 1 +fi + +src="$PAYLOAD_DIR/${name}.apns" +if [[ ! -f "$src" ]]; then + echo "Error: payload not found: $src" >&2 + echo "Available:" >&2 + ls "$PAYLOAD_DIR" | sed 's/\.apns$//' | sed 's/^/ /' >&2 + exit 1 +fi + +if [[ -z "$did" ]]; then + echo "Error: missing recipient DID. Pass --did or set BLUESKY_TEST_DID." >&2 + echo " The DID must match the account currently signed in to the app." >&2 + exit 1 +fi + +tmp="$(mktemp -t bluesky-push.XXXXXX).apns" +trap 'rm -f "$tmp"' EXIT +sed "s|__RECIPIENT_DID__|$did|g" "$src" > "$tmp" + +echo "Pushing '$name' to $device ($bundle)" +xcrun simctl push "$device" "$bundle" "$tmp" +echo "Delivered."