+
+
+ Shared Content
+
+
+
+
+ {/* Display errors */}
+ {errors.length > 0 && (
+
+
+ Errors:
+
+
+ {errors.map((error, index) => (
+ -
+ {error}
+
+ ))}
+
+
+ )}
+
+ {/* Display text content */}
+ {sharedData &&
+ (sharedData.title || sharedData.text || sharedData.url) && (
+
+ {sharedData.title && (
+
+
+ Title:
+
+ {sharedData.title}
+
+ )}
+
+ {sharedData.text && (
+
+
+ Text:
+
+
+ {sharedData.text}
+
+
+ )}
+
+ {sharedData.url &&
+ (() => {
+ const sanitizedUrl = sanitizeUrl(sharedData.url);
+ if (!sanitizedUrl) {
+ return (
+
+
+ URL:
+
+
+ Invalid or unsafe URL
+
+
+ );
+ }
+ return (
+
+ );
+ })()}
+
+ )}
+
+ {/* Display shared files */}
+ {sharedData && sharedData.files && sharedData.files.length > 0 && (
+
+
+ Attached Files ({sharedData.files.length})
+
+
+
+ {sharedData.files.map((file, index) => (
+
+ {/* Image preview */}
+ {file.dataUrl &&
+ file.type.startsWith("image/") &&
+ (() => {
+ const sanitizedDataUrl = sanitizeDataUrl(file.dataUrl);
+ if (!sanitizedDataUrl) return null;
+
+ return (
+

+ );
+ })()}
+
+ {/* File info */}
+
+
+
+ {file.name}
+
+
+ {formatFileSize(file.size)}
+
+
+
+ {file.type.split("/")[1]?.toUpperCase() || "FILE"}
+
+
+
+ ))}
+
+
+ )}
+
+ );
+}
diff --git a/src/pages/ShareTarget.utils.ts b/src/pages/ShareTarget.utils.ts
new file mode 100644
index 0000000..80a55d0
--- /dev/null
+++ b/src/pages/ShareTarget.utils.ts
@@ -0,0 +1,49 @@
+// SPDX-FileCopyrightText: 2025 SecPal
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+/**
+ * Handles Share Target API messages from the Service Worker.
+ * Extracted as a pure function for testability.
+ *
+ * @param event - MessageEvent from Service Worker
+ * @param shareId - Current session's share ID (from URL param)
+ * @param loadSharedData - Callback to reload shared data after files are received (production: () => void)
+ * @param setErrors - Callback to update error state (production: (errors: string[] | ((prev: string[]) => string[])) => void)
+ */
+export function handleShareTargetMessage(
+ event: MessageEvent,
+ shareId: string | null,
+ loadSharedData: any, // eslint-disable-line @typescript-eslint/no-explicit-any
+ setErrors: any // eslint-disable-line @typescript-eslint/no-explicit-any
+): void {
+ if (!event.data) return;
+
+ const { type, shareId: messageShareId } = event.data;
+
+ if (type === "SHARE_TARGET_FILES") {
+ // Verify shareId matches to prevent cross-session contamination
+ if (shareId && messageShareId && shareId !== messageShareId) {
+ console.warn(
+ `Share ID mismatch: expected ${shareId}, got ${messageShareId}`
+ );
+ return;
+ }
+
+ // Store files in sessionStorage so they persist across reloads
+ sessionStorage.setItem(
+ "share-target-files",
+ JSON.stringify(event.data.files)
+ );
+
+ // Trigger reload of shared data
+ loadSharedData();
+ } else if (type === "SHARE_TARGET_ERROR") {
+ const error = event.data.error || "Unknown error";
+
+ // Only add error if shareId matches (or if no shareId provided)
+ if (!shareId || !messageShareId || shareId === messageShareId) {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ setErrors((prev: any) => [...prev, error]);
+ }
+ }
+}
diff --git a/src/sw.ts b/src/sw.ts
new file mode 100644
index 0000000..664ed73
--- /dev/null
+++ b/src/sw.ts
@@ -0,0 +1,203 @@
+// SPDX-FileCopyrightText: 2025 SecPal
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+///