Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ tmp-*.mjs
*.png
!apps/web/public/icons/**/*.png
!apps/browser-extension/public/icons/*.png
!apps/browser-extension/safari/**/*.png
apps/browser-extension/safari/**/xcuserdata/
apps/browser-extension/safari/**/build/
*.jpg
*.jpeg
*.gif
Expand Down
60 changes: 60 additions & 0 deletions apps/browser-extension/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,66 @@ page and in HTTP/HTTPS frames, including cross-origin frames where Chrome
permits extension injection. Frames Chrome refuses to inject into and closed
shadow roots are not inspected.

## Safari (iPad / iPhone / Mac)

The Safari version reuses the same Dispatch pairing, agents, and submission
system with a mobile-first flow: the extension popup handles connect and
"Select element"; picking (tap + parent/child refine), the comment, and Send
all happen in a transient in-page overlay that removes itself when done. The
page only ignores taps while you are aiming — scrolling always works, and the
comment card releases the page entirely.

Build the web extension bundle:

```sh
pnpm --filter @dispatch/browser-extension build:safari
```

The output lands in `apps/browser-extension/dist/safari/unpacked`, which the
checked-in Xcode project references directly — rebuilding the bundle is enough
for the next Xcode build to pick it up.

### Run on the iPad simulator

Open `apps/browser-extension/safari/Dispatch Feedback/Dispatch Feedback.xcodeproj`,
select an iPad simulator, and Run. In the simulator: Settings → Apps →
Safari → Extensions → Dispatch Browser Feedback → enable. Then open Safari,
tap the extension (puzzle) button in the address bar, and open Dispatch
feedback.

### Distribute through TestFlight

The project is preconfigured with automatic signing for team `ML8BQ6D727`
(the same team the Mac release binaries sign with) and bundle IDs
`dev.bradharris.dispatch.feedback` / `dev.bradharris.dispatch.feedback.extension`.

One-time setup: create the app record in App Store Connect (My Apps → New
App → iOS, bundle ID `dev.bradharris.dispatch.feedback`). If this Mac has no Apple
Distribution certificate yet, Xcode offers to create one during the first
Distribute App.

Per release:

1. `pnpm --filter @dispatch/browser-extension build:safari`
2. Verify `MARKETING_VERSION` matches `package.json` (a vitest check enforces
this) and bump `CURRENT_PROJECT_VERSION` (build number) for each upload.
3. Select "Any iOS Device (arm64)" → Product → Archive → Distribute App →
TestFlight & App Store Connect. (CLI equivalent: `xcodebuild archive` +
`xcodebuild -exportArchive` with an App Store Connect API key via
`-authenticationKeyPath/-authenticationKeyID/-authenticationKeyIssuerID`;
headless export cannot mint distribution profiles without one.)
4. In App Store Connect, add yourself as an internal tester and install the
build via TestFlight on the iPad.
5. On the iPad: Settings → Apps → Safari → Extensions → enable Dispatch
Browser Feedback, then allow it on the sites you want to inspect (or
"Other Websites" for everything). Site access can also be granted in-page
from the aA / puzzle menu the first time you use the picker.

Pairing works the same as Chrome: open the extension popup, enter your
Dispatch URL, approve the code in Dispatch settings. The popup may close while
the approval tab is open — pairing continues in the background; reopen the
popup to see the connected state.

## Release checks

The manifest and package versions must match; an extension test enforces this.
Expand Down
25 changes: 25 additions & 0 deletions apps/browser-extension/manifest.safari.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"manifest_version": 3,
"name": "Dispatch Browser Feedback",
"description": "Select live page elements and send focused feedback to a Dispatch agent.",
"version": "0.29.0",
"permissions": ["scripting", "storage", "activeTab"],
"host_permissions": ["http://*/*", "https://*/*"],
"icons": {
"16": "icons/icon-16.png",
"32": "icons/icon-32.png",
"48": "icons/icon-48.png",
"128": "icons/icon-128.png"
},
"action": {
"default_title": "Dispatch feedback",
"default_popup": "popup.html",
"default_icon": {
"16": "icons/icon-16.png",
"32": "icons/icon-32.png"
}
},
"background": {
"service_worker": "background.js"
}
}
1 change: 1 addition & 0 deletions apps/browser-extension/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"type": "module",
"scripts": {
"build": "vite build && vite build --config vite.picker.config.ts",
"build:safari": "vite build --config vite.safari.config.ts && vite build --config vite.safari.background.config.ts && vite build --config vite.safari.overlay.config.ts",
"package": "pnpm run build && rm -f dist/dispatch-browser-feedback.zip && cd dist/unpacked && zip -qr ../dispatch-browser-feedback.zip .",
"check": "tsc --noEmit",
"test": "vitest run"
Expand Down
12 changes: 12 additions & 0 deletions apps/browser-extension/popup.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Dispatch feedback</title>
</head>
<body>
<main id="app"></main>
<script type="module" src="/src/safari/popup.ts"></script>
</body>
</html>
2 changes: 1 addition & 1 deletion apps/browser-extension/public/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"manifest_version": 3,
"name": "Dispatch Browser Feedback",
"description": "Select live page elements and send focused feedback to a Dispatch agent.",
"version": "0.28.4",
"version": "0.29.0",
"minimum_chrome_version": "114",
"permissions": ["scripting", "storage", "sidePanel"],
"optional_host_permissions": ["http://*/*", "https://*/*"],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSExtension</key>
<dict>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.Safari.web-extension</string>
<key>NSExtensionPrincipalClass</key>
<string>$(PRODUCT_MODULE_NAME).SafariWebExtensionHandler</string>
</dict>
</dict>
</plist>
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
//
// SafariWebExtensionHandler.swift
// Dispatch Feedback Extension
//
// Created by Brad Harris on 7/16/26.
//

import SafariServices
import os.log

class SafariWebExtensionHandler: NSObject, NSExtensionRequestHandling {

func beginRequest(with context: NSExtensionContext) {
let request = context.inputItems.first as? NSExtensionItem

let profile: UUID?
if #available(iOS 17.0, macOS 14.0, *) {
profile = request?.userInfo?[SFExtensionProfileKey] as? UUID
} else {
profile = request?.userInfo?["profile"] as? UUID
}

let message: Any?
if #available(iOS 15.0, macOS 11.0, *) {
message = request?.userInfo?[SFExtensionMessageKey]
} else {
message = request?.userInfo?["message"]
}

os_log(.default, "Received message from browser.runtime.sendNativeMessage: %@ (profile: %@)", String(describing: message), profile?.uuidString ?? "none")

let response = NSExtensionItem()
if #available(iOS 15.0, macOS 11.0, *) {
response.userInfo = [ SFExtensionMessageKey: [ "echo": message ] ]
} else {
response.userInfo = [ "message": [ "echo": message ] ]
}

context.completeRequest(returningItems: [ response ], completionHandler: nil)
}

}
Loading
Loading