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.
- package/README.md +84 -102
- package/dist/content.d.mts +3 -3
- package/dist/content.d.ts +3 -3
- package/dist/content.js +296 -271
- package/dist/content.js.map +1 -1
- package/dist/content.mjs +298 -273
- package/dist/content.mjs.map +1 -1
- package/dist/drawer.d.mts +3 -3
- package/dist/drawer.d.ts +3 -3
- package/dist/drawer.js +298 -275
- package/dist/drawer.js.map +1 -1
- package/dist/drawer.mjs +300 -277
- package/dist/drawer.mjs.map +1 -1
- package/dist/headless-B7I228Dt.d.mts +98 -0
- package/dist/headless-BiSYHizs.d.ts +98 -0
- package/dist/headless.d.mts +3 -50
- package/dist/headless.d.ts +3 -50
- package/dist/headless.js +165 -0
- package/dist/headless.js.map +1 -1
- package/dist/headless.mjs +163 -1
- package/dist/headless.mjs.map +1 -1
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +339 -277
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +329 -278
- package/dist/index.mjs.map +1 -1
- package/dist/{platforms-DU1DVDFq.d.mts → platforms-omqzPfYX.d.mts} +17 -23
- package/dist/{platforms-DU1DVDFq.d.ts → platforms-omqzPfYX.d.ts} +17 -23
- package/package.json +24 -7
- package/src/ShareSheetContent.tsx +157 -311
- package/src/ShareSheetDrawer.tsx +2 -4
- package/src/__tests__/hooks.test.ts +203 -0
- package/src/__tests__/og-fetcher.test.ts +144 -0
- package/src/__tests__/platforms.test.ts +148 -0
- package/src/__tests__/setup.ts +22 -0
- package/src/__tests__/share-functions.test.ts +152 -0
- package/src/__tests__/utils.test.ts +64 -0
- package/src/headless.ts +4 -1
- package/src/hooks.ts +60 -2
- package/src/index.ts +20 -4
- package/src/og-fetcher.ts +64 -0
- package/src/share-functions.ts +25 -1
- package/src/types.ts +17 -24
- 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
|
-
|
|
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 {
|
|
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
|
+
}
|
package/src/share-functions.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import {
|
|
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
|
+
|