Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Changelog

## 0.0.11-beta.4 — 2026-06-10

### Fixes
- Stop the `/audit` ShareDock's "share on X" / "share on LinkedIn" buttons from opening the Windows OS share dialog on desktop Chromium / Edge. `lib/share-card.ts` `shareCardNative()` now early-returns `false` on non-mobile devices (detected via `navigator.userAgentData.mobile` with a UA-string fallback for Safari / Firefox + a touch-points check for iPadOS 13+) so the existing clipboard + `x.com/intent/tweet` / `linkedin.com/sharing/share-offsite` fallback runs instead. On mobile, the system share sheet still fires as before because it actually surfaces the X / LinkedIn apps as targets. +1 test for the desktop short-circuit, existing happy-path tests opt into mobile via `navigator.userAgentData.mobile = true`.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

## 0.0.11-beta.3 — 2026-06-09

### Dependencies
Expand Down
20 changes: 19 additions & 1 deletion __tests__/lib/share-card.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ describe("lib/share-card", () => {
vi.restoreAllMocks();
delete (globalThis as { ClipboardItem?: unknown }).ClipboardItem;
Object.defineProperty(navigator, "clipboard", { configurable: true, value: undefined });
Object.defineProperty(navigator, "userAgentData", { configurable: true, value: undefined });
});

it("returns 'clipboard' when navigator.clipboard.write succeeds", async () => {
Expand Down Expand Up @@ -120,10 +121,11 @@ describe("lib/share-card", () => {
}
});

it("shareCardNative returns true when navigator.share resolves", async () => {
it("shareCardNative returns true when navigator.share resolves on mobile", async () => {
const share = vi.fn().mockResolvedValue(undefined);
Object.defineProperty(navigator, "share", { configurable: true, value: share });
Object.defineProperty(navigator, "canShare", { configurable: true, value: () => true });
Object.defineProperty(navigator, "userAgentData", { configurable: true, value: { mobile: true } });

const ok = await shareCardNative(makeBlob(), "x.png", "hello");

Expand All @@ -136,9 +138,25 @@ describe("lib/share-card", () => {
const share = vi.fn().mockRejectedValue(abort);
Object.defineProperty(navigator, "share", { configurable: true, value: share });
Object.defineProperty(navigator, "canShare", { configurable: true, value: () => true });
Object.defineProperty(navigator, "userAgentData", { configurable: true, value: { mobile: true } });

const ok = await shareCardNative(makeBlob(), "x.png", "hello");

expect(ok).toBe(false);
});

it("shareCardNative returns false on desktop without invoking navigator.share", async () => {
// Windows Chromium / Edge expose `navigator.share` but the OS share sheet
// doesn't include X / LinkedIn as targets, so we skip native entirely on
// desktop and let the caller's clipboard + intent-URL fallback run.
const share = vi.fn().mockResolvedValue(undefined);
Object.defineProperty(navigator, "share", { configurable: true, value: share });
Object.defineProperty(navigator, "canShare", { configurable: true, value: () => true });
Object.defineProperty(navigator, "userAgentData", { configurable: true, value: { mobile: false } });

const ok = await shareCardNative(makeBlob(), "x.png", "hello");

expect(ok).toBe(false);
expect(share).not.toHaveBeenCalled();
});
});
28 changes: 26 additions & 2 deletions lib/share-card.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,34 @@

export type ShareCardMethod = "native" | "clipboard" | "download" | "failed";

/**
* Best-effort "is this a mobile device" check. On mobile, the system share
* sheet exposes the actual X / LinkedIn apps as targets so `navigator.share`
* is a one-tap UX win. On desktop (notably Windows Chromium / Edge) the same
* call opens the OS share dialog — useless for X / LinkedIn — so we
* deliberately skip it and fall back to clipboard + intent URL.
*
* Prefers `userAgentData.mobile` (Chromium UA-Client-Hints, no sniffing),
* falls back to a tight UA pattern for Safari / Firefox, and treats
* iPadOS 13+ (UA says Mac but multi-touch is available) as mobile.
*/
function isLikelyMobile(): boolean {
if (typeof navigator === "undefined") return false;
type UADataNav = Navigator & { userAgentData?: { mobile?: boolean } };
const uaData = (navigator as UADataNav).userAgentData;
if (uaData && typeof uaData.mobile === "boolean") return uaData.mobile;
const ua = navigator.userAgent || "";
if (/Android|iPhone|iPad|iPod|webOS|BlackBerry|IEMobile|Opera Mini/i.test(ua)) return true;
if (ua.includes("Mac") && typeof navigator.maxTouchPoints === "number" && navigator.maxTouchPoints > 1) return true;
return false;
}

/**
* Try the native Web Share API with a file attachment. Returns `true` on
* success, `false` if the API is unavailable, files-sharing isn't
* supported, or the user dismissed the sheet. Caller should fall back to
* `copyOrDownloadCard` on `false`.
* supported, the device is desktop (where the OS share sheet doesn't
* include X / LinkedIn), or the user dismissed the sheet. Caller should
* fall back to `copyOrDownloadCard` on `false`.
*/
export async function shareCardNative(
blob: Blob,
Expand All @@ -34,6 +57,7 @@ export async function shareCardNative(
if (typeof navigator === "undefined" || typeof navigator.share !== "function") {
return false;
}
if (!isLikelyMobile()) return false;
const file = new File([blob], filename, { type: "image/png" });
// Some browsers (older Chrome desktop) expose `navigator.share` but reject
// file payloads. `canShare({ files })` gates that cleanly.
Expand Down