react-sharesheet 1.0.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/README.md +84 -102
  2. package/dist/content.d.mts +3 -3
  3. package/dist/content.d.ts +3 -3
  4. package/dist/content.js +296 -271
  5. package/dist/content.js.map +1 -1
  6. package/dist/content.mjs +298 -273
  7. package/dist/content.mjs.map +1 -1
  8. package/dist/drawer.d.mts +3 -3
  9. package/dist/drawer.d.ts +3 -3
  10. package/dist/drawer.js +298 -275
  11. package/dist/drawer.js.map +1 -1
  12. package/dist/drawer.mjs +300 -277
  13. package/dist/drawer.mjs.map +1 -1
  14. package/dist/headless-B7I228Dt.d.mts +98 -0
  15. package/dist/headless-BiSYHizs.d.ts +98 -0
  16. package/dist/headless.d.mts +3 -50
  17. package/dist/headless.d.ts +3 -50
  18. package/dist/headless.js +165 -0
  19. package/dist/headless.js.map +1 -1
  20. package/dist/headless.mjs +163 -1
  21. package/dist/headless.mjs.map +1 -1
  22. package/dist/index.d.mts +2 -2
  23. package/dist/index.d.ts +2 -2
  24. package/dist/index.js +339 -277
  25. package/dist/index.js.map +1 -1
  26. package/dist/index.mjs +329 -278
  27. package/dist/index.mjs.map +1 -1
  28. package/dist/{platforms-DU1DVDFq.d.mts → platforms-omqzPfYX.d.mts} +17 -23
  29. package/dist/{platforms-DU1DVDFq.d.ts → platforms-omqzPfYX.d.ts} +17 -23
  30. package/package.json +24 -7
  31. package/src/ShareSheetContent.tsx +157 -311
  32. package/src/ShareSheetDrawer.tsx +2 -4
  33. package/src/__tests__/hooks.test.ts +203 -0
  34. package/src/__tests__/og-fetcher.test.ts +144 -0
  35. package/src/__tests__/platforms.test.ts +148 -0
  36. package/src/__tests__/setup.ts +22 -0
  37. package/src/__tests__/share-functions.test.ts +152 -0
  38. package/src/__tests__/utils.test.ts +64 -0
  39. package/src/headless.ts +4 -1
  40. package/src/hooks.ts +60 -2
  41. package/src/index.ts +20 -4
  42. package/src/og-fetcher.ts +64 -0
  43. package/src/share-functions.ts +25 -1
  44. package/src/types.ts +17 -24
  45. package/src/utils.ts +125 -0
@@ -0,0 +1,64 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ import { cn, getSafeUrl, openUrl } from "../utils";
3
+
4
+ describe("utils", () => {
5
+ beforeEach(() => {
6
+ vi.clearAllMocks();
7
+ });
8
+
9
+ describe("cn", () => {
10
+ it("should merge class names", () => {
11
+ expect(cn("foo", "bar")).toBe("foo bar");
12
+ });
13
+
14
+ it("should handle conditional classes", () => {
15
+ expect(cn("foo", false && "bar", "baz")).toBe("foo baz");
16
+ });
17
+
18
+ it("should handle undefined values", () => {
19
+ expect(cn("foo", undefined, "bar")).toBe("foo bar");
20
+ });
21
+
22
+ it("should merge Tailwind classes correctly", () => {
23
+ expect(cn("p-4", "p-2")).toBe("p-2");
24
+ expect(cn("text-red-500", "text-blue-500")).toBe("text-blue-500");
25
+ });
26
+
27
+ it("should handle empty inputs", () => {
28
+ expect(cn()).toBe("");
29
+ expect(cn("")).toBe("");
30
+ });
31
+ });
32
+
33
+ describe("getSafeUrl", () => {
34
+ it("should return URL if provided", () => {
35
+ expect(getSafeUrl("https://example.com")).toBe("https://example.com");
36
+ });
37
+
38
+ it("should return URL as-is (no trimming)", () => {
39
+ // Current implementation doesn't trim
40
+ expect(getSafeUrl(" https://example.com ")).toBe(" https://example.com ");
41
+ });
42
+
43
+ it("should return current location for empty URL", () => {
44
+ const originalHref = window.location.href;
45
+ expect(getSafeUrl("")).toBe(originalHref);
46
+ });
47
+
48
+ it("should return current location for falsy URL", () => {
49
+ const originalHref = window.location.href;
50
+ expect(getSafeUrl("")).toBe(originalHref);
51
+ });
52
+ });
53
+
54
+ describe("openUrl", () => {
55
+ it("should open URL in new tab with security options", () => {
56
+ openUrl("https://example.com");
57
+ expect(window.open).toHaveBeenCalledWith(
58
+ "https://example.com",
59
+ "_blank",
60
+ "noopener,noreferrer"
61
+ );
62
+ });
63
+ });
64
+ });
package/src/headless.ts CHANGED
@@ -1,6 +1,9 @@
1
1
  // Headless exports - just the hook and utilities, no styled components
2
2
 
3
- export { useShareSheet, useShareMenu, type UseShareSheetOptions, type UseShareMenuOptions } from "./hooks";
3
+ export { useShareSheet, useShareMenu, useOGData, type UseShareSheetOptions, type UseShareMenuOptions } from "./hooks";
4
+
5
+ // OG Data fetcher
6
+ export { fetchOGData, clearOGCache, type OGData } from "./og-fetcher";
4
7
 
5
8
  export {
6
9
  shareToWhatsApp,
package/src/hooks.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  "use client";
2
2
 
3
- import { useMemo, useState, useCallback } from "react";
4
- import { getSafeUrl } from "./utils";
3
+ import { useMemo, useState, useCallback, useEffect } from "react";
4
+ import { getSafeUrl, isMobileDevice, getAllPlatformAvailability } from "./utils";
5
+ import { fetchOGData, type OGData } from "./og-fetcher";
5
6
  import {
6
7
  shareToWhatsApp,
7
8
  shareToTelegram,
@@ -58,6 +59,14 @@ export function useShareSheet({
58
59
  return typeof navigator !== "undefined" && "share" in navigator;
59
60
  }, []);
60
61
 
62
+ const isMobile = useMemo(() => {
63
+ return isMobileDevice();
64
+ }, []);
65
+
66
+ const platformAvailability = useMemo(() => {
67
+ return getAllPlatformAvailability();
68
+ }, []);
69
+
61
70
  const safeUrl = getSafeUrl(shareUrl);
62
71
 
63
72
  const copyLink = useCallback(async () => {
@@ -167,6 +176,8 @@ export function useShareSheet({
167
176
  copied,
168
177
  downloading,
169
178
  safeUrl,
179
+ isMobile,
180
+ platformAvailability,
170
181
  copyLink,
171
182
  nativeShare,
172
183
  downloadFile,
@@ -185,6 +196,53 @@ export function useShareSheet({
185
196
  };
186
197
  }
187
198
 
199
+ /**
200
+ * Hook to fetch OG (Open Graph) data from a URL.
201
+ * Automatically fetches and caches OG metadata for link previews.
202
+ */
203
+ export function useOGData(url: string | undefined): {
204
+ ogData: OGData | null;
205
+ loading: boolean;
206
+ error: string | null;
207
+ } {
208
+ const [ogData, setOgData] = useState<OGData | null>(null);
209
+ const [loading, setLoading] = useState(false);
210
+ const [error, setError] = useState<string | null>(null);
211
+
212
+ useEffect(() => {
213
+ if (!url) {
214
+ setOgData(null);
215
+ setLoading(false);
216
+ setError(null);
217
+ return;
218
+ }
219
+
220
+ let cancelled = false;
221
+ setLoading(true);
222
+ setError(null);
223
+
224
+ fetchOGData(url)
225
+ .then((data) => {
226
+ if (!cancelled) {
227
+ setOgData(data);
228
+ setLoading(false);
229
+ }
230
+ })
231
+ .catch((err) => {
232
+ if (!cancelled) {
233
+ setError(err instanceof Error ? err.message : "Failed to fetch OG data");
234
+ setLoading(false);
235
+ }
236
+ });
237
+
238
+ return () => {
239
+ cancelled = true;
240
+ };
241
+ }, [url]);
242
+
243
+ return { ogData, loading, error };
244
+ }
245
+
188
246
  // Legacy export for backwards compatibility
189
247
  /** @deprecated Use useShareSheet instead */
190
248
  export const useShareMenu = useShareSheet;
package/src/index.ts CHANGED
@@ -3,7 +3,10 @@ export { ShareSheetContent, ShareMenuContent } from "./ShareSheetContent";
3
3
  export { ShareSheetDrawer, ShareMenuDrawer } from "./ShareSheetDrawer";
4
4
 
5
5
  // Headless hook
6
- export { useShareSheet, useShareMenu, type UseShareSheetOptions, type UseShareMenuOptions } from "./hooks";
6
+ export { useShareSheet, useShareMenu, useOGData, type UseShareSheetOptions, type UseShareMenuOptions } from "./hooks";
7
+
8
+ // OG Data fetcher
9
+ export { fetchOGData, clearOGCache, type OGData } from "./og-fetcher";
7
10
 
8
11
  // Types
9
12
  export type {
@@ -21,8 +24,7 @@ export type {
21
24
  ShareButtonConfig,
22
25
  UseShareSheetReturn,
23
26
  UseShareMenuReturn,
24
- PreviewType,
25
- PreviewConfig,
27
+ PlatformAvailability,
26
28
  } from "./types";
27
29
 
28
30
  // CSS Variables for UI (drawer, title, etc.)
@@ -49,7 +51,21 @@ export {
49
51
  } from "./platforms";
50
52
 
51
53
  // Utility functions for custom implementations
52
- export { cn, openUrl, getSafeUrl } from "./utils";
54
+ export {
55
+ cn,
56
+ openUrl,
57
+ getSafeUrl,
58
+ // Device detection
59
+ isMobileDevice,
60
+ isIOSDevice,
61
+ isAndroidDevice,
62
+ // Platform availability
63
+ checkPlatformAvailability,
64
+ getAllPlatformAvailability,
65
+ warnUnavailablePlatform,
66
+ MOBILE_ONLY_PLATFORMS,
67
+ MOBILE_PREFERRED_PLATFORMS,
68
+ } from "./utils";
53
69
 
54
70
  // Individual share functions
55
71
  export {
@@ -0,0 +1,64 @@
1
+ // OG Data fetcher using Microlink API (free, no API key required)
2
+
3
+ export interface OGData {
4
+ title?: string;
5
+ description?: string;
6
+ image?: string;
7
+ url?: string;
8
+ siteName?: string;
9
+ }
10
+
11
+ export interface OGFetchResult {
12
+ data: OGData | null;
13
+ loading: boolean;
14
+ error: string | null;
15
+ }
16
+
17
+ // Cache to avoid re-fetching the same URL
18
+ const ogCache = new Map<string, OGData>();
19
+
20
+ export async function fetchOGData(url: string): Promise<OGData | null> {
21
+ // Check cache first
22
+ if (ogCache.has(url)) {
23
+ return ogCache.get(url)!;
24
+ }
25
+
26
+ try {
27
+ // Use Microlink API to fetch OG data (free tier, no API key needed)
28
+ const apiUrl = `https://api.microlink.io?url=${encodeURIComponent(url)}`;
29
+ const response = await fetch(apiUrl);
30
+
31
+ if (!response.ok) {
32
+ throw new Error(`Failed to fetch OG data: ${response.status}`);
33
+ }
34
+
35
+ const json = await response.json();
36
+
37
+ if (json.status !== "success" || !json.data) {
38
+ return null;
39
+ }
40
+
41
+ const { title, description, image, url: canonicalUrl, publisher } = json.data;
42
+
43
+ const ogData: OGData = {
44
+ title: title || undefined,
45
+ description: description || undefined,
46
+ image: image?.url || undefined,
47
+ url: canonicalUrl || url,
48
+ siteName: publisher || undefined,
49
+ };
50
+
51
+ // Cache the result
52
+ ogCache.set(url, ogData);
53
+
54
+ return ogData;
55
+ } catch (error) {
56
+ console.warn("[react-sharesheet] Failed to fetch OG data:", error);
57
+ return null;
58
+ }
59
+ }
60
+
61
+ // Clear cache (useful for testing or forcing refresh)
62
+ export function clearOGCache(): void {
63
+ ogCache.clear();
64
+ }
@@ -1,4 +1,8 @@
1
- import { openUrl } from "./utils";
1
+ import {
2
+ openUrl,
3
+ checkPlatformAvailability,
4
+ warnUnavailablePlatform,
5
+ } from "./utils";
2
6
 
3
7
  export function shareToWhatsApp(url: string, text: string) {
4
8
  const encoded = encodeURIComponent(`${text}\n${url}`);
@@ -23,14 +27,29 @@ export function shareToFacebook(url: string) {
23
27
  }
24
28
 
25
29
  export function openInstagram() {
30
+ const availability = checkPlatformAvailability("instagram");
31
+ if (!availability.available) {
32
+ warnUnavailablePlatform("instagram", availability.reason!);
33
+ // Still attempt to open - it may work on some desktop browsers with app installed
34
+ }
26
35
  window.location.href = "instagram://";
27
36
  }
28
37
 
29
38
  export function openTikTok() {
39
+ const availability = checkPlatformAvailability("tiktok");
40
+ if (!availability.available) {
41
+ warnUnavailablePlatform("tiktok", availability.reason!);
42
+ // Still attempt to open - it may work on some desktop browsers with app installed
43
+ }
30
44
  window.location.href = "tiktok://";
31
45
  }
32
46
 
33
47
  export function openThreads() {
48
+ const availability = checkPlatformAvailability("threads");
49
+ if (!availability.available) {
50
+ warnUnavailablePlatform("threads", availability.reason!);
51
+ // Still attempt to open - it may work on some desktop browsers with app installed
52
+ }
34
53
  window.location.href = "threads://";
35
54
  }
36
55
 
@@ -40,6 +59,11 @@ export function shareToSnapchat(url: string) {
40
59
  }
41
60
 
42
61
  export function shareViaSMS(url: string, text: string) {
62
+ const availability = checkPlatformAvailability("sms");
63
+ if (!availability.available) {
64
+ warnUnavailablePlatform("sms", availability.reason!);
65
+ // Still attempt to open - it may work on some devices
66
+ }
43
67
  const body = encodeURIComponent(`${text}\n${url}`);
44
68
  window.location.href = `sms:?body=${body}`;
45
69
  }
package/src/types.ts CHANGED
@@ -97,32 +97,13 @@ export interface ShareSheetDrawerClassNames extends ShareSheetContentClassNames
97
97
  trigger?: string;
98
98
  }
99
99
 
100
- /** Preview content type */
101
- export type PreviewType = "image" | "video" | "audio" | "file" | "link" | "auto";
102
-
103
- /** Preview configuration */
104
- export interface PreviewConfig {
105
- /** URL of the content to preview */
106
- url: string;
107
- /** Type of content (auto-detected if not provided) */
108
- type?: PreviewType;
109
- /** Filename to display (for file/audio types) */
110
- filename?: string;
111
- /** Alt text for images */
112
- alt?: string;
113
- /** Poster image for videos */
114
- poster?: string;
115
- }
116
-
117
100
  export interface ShareSheetContentProps {
118
101
  /** Title displayed at the top of the sheet */
119
102
  title?: string;
120
- /** URL to share */
103
+ /** URL to share (OG preview will be fetched automatically) */
121
104
  shareUrl: string;
122
105
  /** Text to share alongside the URL */
123
106
  shareText: string;
124
- /** Preview of content being shared (string URL or config object) */
125
- preview?: string | PreviewConfig | null;
126
107
  /** Optional URL for download functionality */
127
108
  downloadUrl?: string | null;
128
109
  /** Filename for downloaded file */
@@ -175,6 +156,14 @@ export interface ShareButtonConfig {
175
156
  condition?: boolean;
176
157
  }
177
158
 
159
+ /** Platform availability status */
160
+ export interface PlatformAvailability {
161
+ /** Whether the platform is available on this device */
162
+ available: boolean;
163
+ /** Reason why platform is unavailable (if applicable) */
164
+ reason?: string;
165
+ }
166
+
178
167
  /** Return type of useShareSheet hook */
179
168
  export interface UseShareSheetReturn {
180
169
  /** Whether the browser supports native share */
@@ -185,6 +174,10 @@ export interface UseShareSheetReturn {
185
174
  downloading: boolean;
186
175
  /** The safe URL (falls back to current page URL) */
187
176
  safeUrl: string;
177
+ /** Whether the current device is mobile */
178
+ isMobile: boolean;
179
+ /** Availability status for each platform */
180
+ platformAvailability: Record<ShareOption, PlatformAvailability>;
188
181
  /** Copy the share URL to clipboard */
189
182
  copyLink: () => Promise<void>;
190
183
  /** Trigger native share dialog */
@@ -199,15 +192,15 @@ export interface UseShareSheetReturn {
199
192
  shareX: () => void;
200
193
  /** Share to Facebook */
201
194
  shareFacebook: () => void;
202
- /** Open Instagram app */
195
+ /** Open Instagram app (mobile only - will warn on desktop) */
203
196
  shareInstagram: () => void;
204
- /** Open TikTok app */
197
+ /** Open TikTok app (mobile only - will warn on desktop) */
205
198
  shareTikTok: () => void;
206
- /** Open Threads app */
199
+ /** Open Threads app (mobile only - will warn on desktop) */
207
200
  shareThreads: () => void;
208
201
  /** Share to Snapchat */
209
202
  shareSnapchat: () => void;
210
- /** Share via SMS */
203
+ /** Share via SMS (mobile only - will warn on desktop) */
211
204
  shareSMS: () => void;
212
205
  /** Share via Email */
213
206
  shareEmail: () => void;
package/src/utils.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { clsx, type ClassValue } from "clsx";
2
2
  import { twMerge } from "tailwind-merge";
3
+ import type { ShareOption, PlatformAvailability } from "./types";
3
4
 
4
5
  export function cn(...inputs: ClassValue[]) {
5
6
  return twMerge(clsx(inputs));
@@ -13,3 +14,127 @@ export function getSafeUrl(shareUrl: string): string {
13
14
  return shareUrl || (typeof window !== "undefined" ? window.location.href : "");
14
15
  }
15
16
 
17
+ /**
18
+ * Detect if the current device is a mobile device.
19
+ * Uses user agent detection as the primary method.
20
+ */
21
+ export function isMobileDevice(): boolean {
22
+ if (typeof navigator === "undefined") return false;
23
+
24
+ const userAgent = navigator.userAgent || navigator.vendor || "";
25
+
26
+ // Check for mobile user agents
27
+ const mobileRegex = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini|Mobile|mobile|CriOS/i;
28
+
29
+ // Also check for touch capability as a secondary signal
30
+ const hasTouch = typeof window !== "undefined" && (
31
+ "ontouchstart" in window ||
32
+ navigator.maxTouchPoints > 0
33
+ );
34
+
35
+ // User agent is more reliable than touch detection alone
36
+ // (many desktop browsers support touch)
37
+ return mobileRegex.test(userAgent);
38
+ }
39
+
40
+ /**
41
+ * Detect if the current device is iOS
42
+ */
43
+ export function isIOSDevice(): boolean {
44
+ if (typeof navigator === "undefined") return false;
45
+
46
+ const userAgent = navigator.userAgent || "";
47
+ return /iPhone|iPad|iPod/i.test(userAgent);
48
+ }
49
+
50
+ /**
51
+ * Detect if the current device is Android
52
+ */
53
+ export function isAndroidDevice(): boolean {
54
+ if (typeof navigator === "undefined") return false;
55
+
56
+ const userAgent = navigator.userAgent || "";
57
+ return /Android/i.test(userAgent);
58
+ }
59
+
60
+ /** Platforms that require mobile devices (deep links / URL schemes) */
61
+ export const MOBILE_ONLY_PLATFORMS: readonly ShareOption[] = [
62
+ "instagram",
63
+ "tiktok",
64
+ "threads",
65
+ "sms",
66
+ ] as const;
67
+
68
+ /** Platforms that work better on mobile but may partially work on desktop */
69
+ export const MOBILE_PREFERRED_PLATFORMS: readonly ShareOption[] = [
70
+ "snapchat",
71
+ "whatsapp",
72
+ ] as const;
73
+
74
+ /**
75
+ * Check if a share platform is available on the current device.
76
+ * Returns availability status and reason if unavailable.
77
+ */
78
+ export function checkPlatformAvailability(platform: ShareOption): PlatformAvailability {
79
+ const isMobile = isMobileDevice();
80
+
81
+ // Deep link platforms - require mobile device
82
+ if (MOBILE_ONLY_PLATFORMS.includes(platform)) {
83
+ if (!isMobile) {
84
+ return {
85
+ available: false,
86
+ reason: `${platform} requires a mobile device with the app installed`,
87
+ };
88
+ }
89
+ }
90
+
91
+ // SMS - requires mobile or device with SMS capability
92
+ if (platform === "sms" && !isMobile) {
93
+ return {
94
+ available: false,
95
+ reason: "SMS sharing requires a mobile device",
96
+ };
97
+ }
98
+
99
+ // Native share - check browser support
100
+ if (platform === "native") {
101
+ const canShare = typeof navigator !== "undefined" && "share" in navigator;
102
+ if (!canShare) {
103
+ return {
104
+ available: false,
105
+ reason: "Native share is not supported by this browser",
106
+ };
107
+ }
108
+ }
109
+
110
+ return { available: true };
111
+ }
112
+
113
+ /**
114
+ * Get availability status for all platforms.
115
+ */
116
+ export function getAllPlatformAvailability(): Record<ShareOption, PlatformAvailability> {
117
+ const platforms: ShareOption[] = [
118
+ "native", "copy", "download", "whatsapp", "telegram",
119
+ "instagram", "facebook", "snapchat", "sms", "email",
120
+ "linkedin", "reddit", "x", "tiktok", "threads",
121
+ ];
122
+
123
+ const result: Partial<Record<ShareOption, PlatformAvailability>> = {};
124
+ for (const platform of platforms) {
125
+ result[platform] = checkPlatformAvailability(platform);
126
+ }
127
+
128
+ return result as Record<ShareOption, PlatformAvailability>;
129
+ }
130
+
131
+ /**
132
+ * Log a warning to console when a platform is not available.
133
+ */
134
+ export function warnUnavailablePlatform(platform: ShareOption, reason: string): void {
135
+ console.warn(
136
+ `[react-sharesheet] ${platform} sharing is not available: ${reason}. ` +
137
+ `This share option may not work correctly on this device.`
138
+ );
139
+ }
140
+