@multiplatform.one/platform 6.0.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.
@@ -0,0 +1,62 @@
1
+ import type { ComponentType } from "react";
2
+
3
+ const isPerformanceNow = typeof performance !== "undefined" && performance.now;
4
+ const date = isPerformanceNow ? performance : Date;
5
+ let _previousUnique: number;
6
+
7
+ export function fastUnique() {
8
+ let unique = date.now();
9
+ while (unique === _previousUnique) {
10
+ unique = date.now();
11
+ }
12
+ _previousUnique = unique;
13
+ if (isPerformanceNow) return Date.now() + unique.toString();
14
+ return unique.toString();
15
+ }
16
+
17
+ export function isText(children: unknown) {
18
+ if (Array.isArray(children)) {
19
+ return (
20
+ children.length > 1 &&
21
+ typeof children[0] === "string" &&
22
+ "_owner" in children[1] &&
23
+ "_store" in children[1] &&
24
+ "key" in children[1] &&
25
+ "props" in children[1] &&
26
+ "ref" in children[1] &&
27
+ "type" in children[1]
28
+ );
29
+ }
30
+ return typeof children === "string";
31
+ }
32
+
33
+ export function createWithLayout<LayoutProps>(
34
+ Layout: ComponentType<LayoutProps>,
35
+ extraLayouts: WithLayout[] = [],
36
+ layoutProps?: Omit<LayoutProps, "children">,
37
+ ): WithLayout {
38
+ return ((Component: ComponentType<unknown>) => {
39
+ return flattenLayouts(
40
+ (props: unknown) => (
41
+ <Layout {...(layoutProps as LayoutProps)}>
42
+ <Component {...(props as any)} />
43
+ </Layout>
44
+ ),
45
+ extraLayouts,
46
+ );
47
+ }) as WithLayout;
48
+ }
49
+
50
+ function flattenLayouts<Props>(
51
+ layout: ComponentType<Props>,
52
+ [...layouts]: WithLayout[],
53
+ ): ComponentType<Props> {
54
+ const currentLayout = layouts.pop();
55
+ if (!currentLayout) return layout;
56
+ return flattenLayouts(currentLayout(layout), layouts);
57
+ }
58
+
59
+ export type WithLayout = <Props>(Component: ComponentType<Props>) => ComponentType<Props>;
60
+
61
+ export const isDev =
62
+ process.env.NODE_ENV === "development" || (typeof __DEV__ !== "undefined" && __DEV__);
package/src/index.ts ADDED
@@ -0,0 +1,35 @@
1
+ import { platform } from "./platform/index";
2
+
3
+ export const {
4
+ isAndroid,
5
+ isBrowser,
6
+ isChrome,
7
+ isChromeExtension,
8
+ isClient,
9
+ isExpo,
10
+ isFirefox,
11
+ isFirefoxExtension,
12
+ isIframe,
13
+ isIos,
14
+ isNative,
15
+ isNext,
16
+ isServer,
17
+ isStorybook,
18
+ isTauri,
19
+ isDesktop,
20
+ isTouchable,
21
+ isWeb,
22
+ isWebExtension,
23
+ isWebTouchable,
24
+ isWindowDefined,
25
+ } = platform;
26
+
27
+ export * from "./platform/index";
28
+ export * from "./helpers";
29
+ export * from "./types";
30
+ export * from "./config/index";
31
+ export * from "./utils/cookie";
32
+ export * from "./utils/downloadFile";
33
+ export * from "./utils/clipboard";
34
+ export * from "./utils/openUrl";
35
+ export * from "./utils/lifecycle";
@@ -0,0 +1,10 @@
1
+ import { type Platform, getBroadName, getPreciseName, platformBase } from "./platformBase";
2
+
3
+ export const platform: Platform = {
4
+ ...platformBase,
5
+ isAndroid: true,
6
+ isExpo: true,
7
+ isNative: true,
8
+ };
9
+ platform.preciseName = getPreciseName(platform);
10
+ platform.broadName = getBroadName(platform);
@@ -0,0 +1,10 @@
1
+ import { type Platform, getBroadName, getPreciseName, platformBase } from "./platformBase";
2
+
3
+ export const platform: Platform = {
4
+ ...platformBase,
5
+ isExpo: true,
6
+ isIos: true,
7
+ isNative: true,
8
+ };
9
+ platform.preciseName = getPreciseName(platform);
10
+ platform.broadName = getBroadName(platform);
@@ -0,0 +1,45 @@
1
+ import {
2
+ isChrome,
3
+ isClient,
4
+ isServer,
5
+ isWeb,
6
+ isWebTouchable,
7
+ isWindowDefined,
8
+ } from "@tamagui/constants";
9
+ import { type Platform, getBroadName, getPreciseName, platformBase } from "./platformBase";
10
+
11
+ declare global {
12
+ interface Window {
13
+ __TAURI_INTERNALS__?: unknown;
14
+ __TAURI__?: unknown;
15
+ }
16
+ }
17
+
18
+ const isIframe = (() => {
19
+ if (!isWindowDefined) return false;
20
+ try {
21
+ return window.self !== window.top;
22
+ } catch {
23
+ return true;
24
+ }
25
+ })();
26
+
27
+ export const platform: Platform = {
28
+ ...platformBase,
29
+ isBrowser: false,
30
+ isChrome,
31
+ isClient,
32
+ isIframe,
33
+ isServer,
34
+ isWeb,
35
+ isWebTouchable,
36
+ // Tauri-specific flags - using the same interface structure
37
+ // Platform detection: Tauri injects __TAURI_INTERNALS__ at runtime
38
+ isTauri: true,
39
+ isDesktop: true,
40
+ };
41
+ platform.preciseName = getPreciseName(platform);
42
+ platform.broadName = getBroadName(platform);
43
+
44
+ export type { Platform };
45
+ export type { PlatformName } from "./platformBase";
@@ -0,0 +1,45 @@
1
+ /// <reference types="@types/chrome" />
2
+ /// <reference types="@types/firefox-webext-browser" />
3
+
4
+ import { type Platform, getBroadName, getPreciseName, platformBase } from "./platformBase";
5
+
6
+ declare global {
7
+ interface Window {
8
+ __NEXT_DATA__?: unknown;
9
+ ipc?: unknown;
10
+ }
11
+ }
12
+
13
+ const isIframe = (() => {
14
+ if (!platformBase.isWindowDefined) return false;
15
+ try {
16
+ return window.self !== window.top;
17
+ } catch {
18
+ return true;
19
+ }
20
+ })();
21
+
22
+ const isWebExtension = !!(
23
+ (typeof chrome !== "undefined" && !!chrome?.runtime?.id) ||
24
+ (typeof browser !== "undefined" && !!browser?.runtime?.id)
25
+ );
26
+
27
+ export const platform: Platform = {
28
+ ...platformBase,
29
+ isChromeExtension: typeof chrome !== "undefined" && !!chrome?.runtime?.id,
30
+ isFirefoxExtension: typeof browser !== "undefined" && !!browser?.runtime?.id,
31
+ isIframe,
32
+ isWebExtension,
33
+ isBrowser: !!(platformBase.isWeb && platformBase.isWindowDefined && !isWebExtension),
34
+ isNext:
35
+ platformBase.isWeb &&
36
+ (!platformBase.isWindowDefined || typeof window.__NEXT_DATA__ === "object"),
37
+ isFirefox:
38
+ platformBase.isWindowDefined &&
39
+ !!(window?.navigator?.userAgent?.toLowerCase().indexOf("firefox") > -1),
40
+ };
41
+ platform.preciseName = getPreciseName(platform);
42
+ platform.broadName = getBroadName(platform);
43
+
44
+ export type { Platform };
45
+ export type { PlatformName } from "./platformBase";
@@ -0,0 +1,111 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { getBroadName, getPreciseName, type Platform } from "./platformBase";
3
+
4
+ function makePlatform(overrides: Partial<Platform> = {}): Platform {
5
+ return {
6
+ isAndroid: false,
7
+ isBrowser: false,
8
+ isChrome: false,
9
+ isChromeExtension: false,
10
+ isClient: false,
11
+ isDesktop: false,
12
+ isExpo: false,
13
+ isFirefox: false,
14
+ isFirefoxExtension: false,
15
+ isIframe: false,
16
+ isIos: false,
17
+ isNative: false,
18
+ isNext: false,
19
+ isServer: false,
20
+ isStorybook: false,
21
+ isTauri: false,
22
+ isTouchable: false,
23
+ isWeb: false,
24
+ isWebExtension: false,
25
+ isWebTouchable: false,
26
+ isWindowDefined: false,
27
+ preciseName: "unknown",
28
+ broadName: "unknown",
29
+ ...overrides,
30
+ };
31
+ }
32
+
33
+ describe("getPreciseName", () => {
34
+ it('returns "unknown" when no flags are true', () => {
35
+ expect(getPreciseName(makePlatform())).toBe("unknown");
36
+ });
37
+
38
+ it("returns the first matching flag in priority order (iOS > Android > others)", () => {
39
+ expect(getPreciseName(makePlatform({ isIos: true, isNative: true, isClient: true }))).toBe(
40
+ "ios",
41
+ );
42
+ });
43
+
44
+ it('returns "android" when isAndroid is the most precise', () => {
45
+ expect(getPreciseName(makePlatform({ isAndroid: true, isNative: true }))).toBe("android");
46
+ });
47
+
48
+ it('returns "tauri" for desktop Tauri apps', () => {
49
+ expect(getPreciseName(makePlatform({ isTauri: true, isDesktop: true, isWeb: true }))).toBe(
50
+ "tauri",
51
+ );
52
+ });
53
+
54
+ it('returns "chromeExtension" over generic "chrome"', () => {
55
+ expect(
56
+ getPreciseName(makePlatform({ isChromeExtension: true, isChrome: true, isWeb: true })),
57
+ ).toBe("chromeExtension");
58
+ });
59
+
60
+ it('returns "next" for Next.js environments', () => {
61
+ expect(getPreciseName(makePlatform({ isNext: true, isWeb: true, isClient: true }))).toBe(
62
+ "next",
63
+ );
64
+ });
65
+
66
+ it('returns "chrome" for a plain Chrome browser', () => {
67
+ expect(getPreciseName(makePlatform({ isChrome: true, isWeb: true, isBrowser: true }))).toBe(
68
+ "chrome",
69
+ );
70
+ });
71
+
72
+ it('returns "server" for server-only', () => {
73
+ expect(getPreciseName(makePlatform({ isServer: true }))).toBe("server");
74
+ });
75
+
76
+ it('returns "storybook" when only isStorybook is true', () => {
77
+ expect(getPreciseName(makePlatform({ isStorybook: true }))).toBe("storybook");
78
+ });
79
+ });
80
+
81
+ describe("getBroadName", () => {
82
+ it('returns "unknown" when no flags are true', () => {
83
+ expect(getBroadName(makePlatform())).toBe("unknown");
84
+ });
85
+
86
+ it("returns the last matching flag in reverse priority order (broadest)", () => {
87
+ expect(getBroadName(makePlatform({ isIos: true, isNative: true, isClient: true }))).toBe(
88
+ "client",
89
+ );
90
+ });
91
+
92
+ it('returns "server" (broadest) for a server environment', () => {
93
+ expect(getBroadName(makePlatform({ isNext: true, isWeb: true, isServer: true }))).toBe(
94
+ "server",
95
+ );
96
+ });
97
+
98
+ it('returns "storybook" for storybook-only', () => {
99
+ expect(getBroadName(makePlatform({ isStorybook: true }))).toBe("storybook");
100
+ });
101
+
102
+ it('returns "iframe" when iframe + storybook are set', () => {
103
+ expect(getBroadName(makePlatform({ isStorybook: true, isIframe: true }))).toBe("storybook");
104
+ });
105
+
106
+ it('returns "browser" for a typical browser client', () => {
107
+ expect(
108
+ getBroadName(makePlatform({ isChrome: true, isWeb: true, isBrowser: true, isClient: true })),
109
+ ).toBe("client");
110
+ });
111
+ });
@@ -0,0 +1,129 @@
1
+ import {
2
+ isChrome,
3
+ isClient,
4
+ isServer,
5
+ isTouchable,
6
+ isWeb,
7
+ isWebTouchable,
8
+ isWindowDefined,
9
+ } from "@tamagui/constants";
10
+
11
+ declare global {
12
+ interface Window {
13
+ __STORYBOOK_ADDONS_PREVIEW: unknown;
14
+ }
15
+ }
16
+
17
+ const platformOrder = [
18
+ "Ios",
19
+ "Android",
20
+ "Native",
21
+ "Tauri",
22
+ "Desktop",
23
+ "ChromeExtension",
24
+ "FirefoxExtension",
25
+ "WebExtension",
26
+ "Next",
27
+ "Expo",
28
+ "Chrome",
29
+ "Firefox",
30
+ "Web",
31
+ "Browser",
32
+ "Client",
33
+ "Server",
34
+ "Iframe",
35
+ "Storybook",
36
+ ];
37
+
38
+ export type PlatformName =
39
+ | "ios"
40
+ | "android"
41
+ | "native"
42
+ | "tauri"
43
+ | "desktop"
44
+ | "chromeExtension"
45
+ | "firefoxExtension"
46
+ | "webExtension"
47
+ | "next"
48
+ | "expo"
49
+ | "chrome"
50
+ | "firefox"
51
+ | "web"
52
+ | "browser"
53
+ | "client"
54
+ | "server"
55
+ | "iframe"
56
+ | "storybook"
57
+ | "unknown";
58
+
59
+ export const platformBase = {
60
+ isAndroid: false,
61
+ isBrowser: false,
62
+ isChrome,
63
+ isChromeExtension: false,
64
+ isClient,
65
+ isDesktop: false,
66
+ isExpo: false,
67
+ isFirefox: false,
68
+ isFirefoxExtension: false,
69
+ isIframe: false,
70
+ isIos: false,
71
+ isNative: false,
72
+ isNext: false,
73
+ isServer,
74
+ isStorybook: isWindowDefined && typeof window.__STORYBOOK_ADDONS_PREVIEW === "object",
75
+ isTauri: false,
76
+ isTouchable,
77
+ isWeb,
78
+ isWebExtension: false,
79
+ isWebTouchable,
80
+ isWindowDefined,
81
+ preciseName: "unknown",
82
+ broadName: "unknown",
83
+ } as const;
84
+
85
+ export function getPreciseName(platform: Omit<Platform, "preciseName">) {
86
+ const flags = platform as unknown as Record<string, boolean>;
87
+ for (const name of platformOrder) {
88
+ if (flags[`is${name}`]) {
89
+ return `${name[0].toLowerCase()}${name.slice(1)}` as PlatformName;
90
+ }
91
+ }
92
+ return "unknown";
93
+ }
94
+
95
+ export function getBroadName(platform: Omit<Platform, "broadName">) {
96
+ const flags = platform as unknown as Record<string, boolean>;
97
+ for (const name of platformOrder.slice().reverse()) {
98
+ if (flags[`is${name}`]) {
99
+ return `${name[0].toLowerCase()}${name.slice(1)}` as PlatformName;
100
+ }
101
+ }
102
+ return "unknown";
103
+ }
104
+
105
+ export interface Platform {
106
+ isAndroid: boolean;
107
+ isBrowser: boolean;
108
+ isChrome: boolean;
109
+ isChromeExtension: boolean;
110
+ isClient: boolean;
111
+ isDesktop: boolean;
112
+ isExpo: boolean;
113
+ isFirefox: boolean;
114
+ isFirefoxExtension: boolean;
115
+ isIframe: boolean;
116
+ isIos: boolean;
117
+ isNative: boolean;
118
+ isNext: boolean;
119
+ isServer: boolean;
120
+ isStorybook: boolean;
121
+ isTauri: boolean;
122
+ isTouchable: boolean;
123
+ isWeb: boolean;
124
+ isWebExtension: boolean;
125
+ isWebTouchable: boolean;
126
+ isWindowDefined: boolean;
127
+ broadName: PlatformName;
128
+ preciseName: PlatformName;
129
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * React Native Web type augmentations for PressableStateCallbackType, ViewStyle,
3
+ * TextProps, ViewProps. Include via tsconfig types: "@multiplatform.one/platform/rnw-overrides"
4
+ */
5
+
6
+ import type { MouseEvent } from "react";
7
+
8
+ declare module "react-native" {
9
+ interface PressableStateCallbackType {
10
+ hovered?: boolean;
11
+ focused?: boolean;
12
+ }
13
+ interface ViewStyle {
14
+ transitionProperty?: string;
15
+ transitionDuration?: string;
16
+ }
17
+ interface TextProps {
18
+ accessibilityComponentType?: never;
19
+ accessibilityTraits?: never;
20
+ href?: string;
21
+ hrefAttrs?: {
22
+ rel: "noreferrer";
23
+ target?: "_blank";
24
+ };
25
+ }
26
+ interface ViewProps {
27
+ accessibilityRole?: string;
28
+ href?: string;
29
+ hrefAttrs?: {
30
+ rel: "noreferrer";
31
+ target?: "_blank";
32
+ };
33
+ onClick?: (e: MouseEvent<HTMLAnchorElement, MouseEvent>) => void;
34
+ }
35
+ }
package/src/types.ts ADDED
@@ -0,0 +1,3 @@
1
+ import type { StyleProp as RNStyleProp } from "react-native";
2
+
3
+ export type StyleProp = RNStyleProp<any>;
@@ -0,0 +1,3 @@
1
+ export async function writeToClipboard(_text: string): Promise<boolean> {
2
+ return false;
3
+ }
@@ -0,0 +1,24 @@
1
+ import { describe, expect, it, vi, beforeEach } from "vitest";
2
+ import { writeToClipboard } from "./clipboard";
3
+
4
+ describe("writeToClipboard", () => {
5
+ beforeEach(() => {
6
+ vi.stubGlobal("navigator", {
7
+ clipboard: { writeText: vi.fn().mockResolvedValue(undefined) },
8
+ });
9
+ });
10
+
11
+ it("returns true when clipboard write succeeds", async () => {
12
+ expect(await writeToClipboard("hello")).toBe(true);
13
+ expect(navigator.clipboard.writeText).toHaveBeenCalledWith("hello");
14
+ });
15
+
16
+ it("returns false when clipboard write fails", async () => {
17
+ vi.stubGlobal("navigator", {
18
+ clipboard: {
19
+ writeText: vi.fn().mockRejectedValue(new Error("denied")),
20
+ },
21
+ });
22
+ expect(await writeToClipboard("hello")).toBe(false);
23
+ });
24
+ });
@@ -0,0 +1,8 @@
1
+ export async function writeToClipboard(text: string): Promise<boolean> {
2
+ try {
3
+ await navigator.clipboard.writeText(text);
4
+ return true;
5
+ } catch {
6
+ return false;
7
+ }
8
+ }
@@ -0,0 +1,13 @@
1
+ const store = new Map<string, string>();
2
+
3
+ export function getCookie(name: string): string | null {
4
+ return store.get(name) ?? null;
5
+ }
6
+
7
+ export function setCookie(name: string, value: string, _options?: object): void {
8
+ store.set(name, value);
9
+ }
10
+
11
+ export function deleteCookie(name: string, _path?: string): void {
12
+ store.delete(name);
13
+ }
@@ -0,0 +1,92 @@
1
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
2
+ import { deleteCookie, getCookie, setCookie } from "./cookie";
3
+
4
+ describe("cookie utilities", () => {
5
+ beforeEach(() => {
6
+ Object.defineProperty(document, "cookie", {
7
+ writable: true,
8
+ value: "",
9
+ });
10
+ });
11
+
12
+ afterEach(() => {
13
+ Object.defineProperty(document, "cookie", {
14
+ writable: true,
15
+ value: "",
16
+ });
17
+ });
18
+
19
+ describe("getCookie", () => {
20
+ it("returns null when no cookies are set", () => {
21
+ expect(getCookie("missing")).toBeNull();
22
+ });
23
+
24
+ it("returns the value of an existing cookie", () => {
25
+ document.cookie = "theme=dark";
26
+ expect(getCookie("theme")).toBe("dark");
27
+ });
28
+
29
+ it("handles URL-encoded values", () => {
30
+ document.cookie = "data=hello%20world";
31
+ expect(getCookie("data")).toBe("hello world");
32
+ });
33
+
34
+ it("returns the correct cookie among multiple", () => {
35
+ document.cookie = "a=1; b=2; c=3";
36
+ expect(getCookie("b")).toBe("2");
37
+ });
38
+
39
+ it("escapes regex special characters in cookie names", () => {
40
+ document.cookie = "weird.name[0]=value";
41
+ expect(getCookie("weird.name[0]")).toBe("value");
42
+ });
43
+
44
+ it("returns null for a partial name match", () => {
45
+ document.cookie = "prefix_key=value";
46
+ expect(getCookie("key")).toBeNull();
47
+ });
48
+ });
49
+
50
+ describe("setCookie", () => {
51
+ it("sets a basic cookie with default options", () => {
52
+ setCookie("key", "value");
53
+ expect(document.cookie).toContain("key=value");
54
+ expect(document.cookie).toContain("path=/");
55
+ expect(document.cookie).toContain("SameSite=Lax");
56
+ });
57
+
58
+ it("sets a cookie with expiration days", () => {
59
+ setCookie("session", "abc", { days: 7 });
60
+ expect(document.cookie).toContain("session=abc");
61
+ expect(document.cookie).toContain("expires=");
62
+ });
63
+
64
+ it("sets the Secure flag when specified", () => {
65
+ setCookie("secure_key", "val", { secure: true });
66
+ expect(document.cookie).toContain("Secure");
67
+ });
68
+
69
+ it("sets custom SameSite value", () => {
70
+ setCookie("strict_key", "val", { sameSite: "Strict" });
71
+ expect(document.cookie).toContain("SameSite=Strict");
72
+ });
73
+
74
+ it("URL-encodes name and value", () => {
75
+ setCookie("special key", "value with spaces");
76
+ expect(document.cookie).toContain("special%20key=value%20with%20spaces");
77
+ });
78
+ });
79
+
80
+ describe("deleteCookie", () => {
81
+ it("sets the cookie to expire in the past", () => {
82
+ setCookie("to_delete", "value");
83
+ deleteCookie("to_delete");
84
+ expect(document.cookie).toContain("expires=Thu, 01 Jan 1970");
85
+ });
86
+
87
+ it("uses a custom path", () => {
88
+ deleteCookie("key", "/custom");
89
+ expect(document.cookie).toContain("path=/custom");
90
+ });
91
+ });
92
+ });
@@ -0,0 +1,33 @@
1
+ export function getCookie(name: string): string | null {
2
+ if (typeof document === "undefined") return null;
3
+ const match = document.cookie.match(
4
+ new RegExp("(?:^|; )" + name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + "=([^;]*)"),
5
+ );
6
+ return match ? decodeURIComponent(match[1]) : null;
7
+ }
8
+
9
+ export function setCookie(
10
+ name: string,
11
+ value: string,
12
+ options: {
13
+ days?: number;
14
+ path?: string;
15
+ sameSite?: "Strict" | "Lax" | "None";
16
+ secure?: boolean;
17
+ } = {},
18
+ ): void {
19
+ if (typeof document === "undefined") return;
20
+ const { days, path = "/", sameSite = "Lax", secure = false } = options;
21
+ let cookie = `${encodeURIComponent(name)}=${encodeURIComponent(value)}; path=${path}; SameSite=${sameSite}`;
22
+ if (days != null) {
23
+ const expires = new Date(Date.now() + days * 864e5);
24
+ cookie += `; expires=${expires.toUTCString()}`;
25
+ }
26
+ if (secure) cookie += "; Secure";
27
+ document.cookie = cookie;
28
+ }
29
+
30
+ export function deleteCookie(name: string, path = "/"): void {
31
+ if (typeof document === "undefined") return;
32
+ document.cookie = `${encodeURIComponent(name)}=; path=${path}; expires=Thu, 01 Jan 1970 00:00:00 GMT`;
33
+ }
@@ -0,0 +1,7 @@
1
+ export function downloadFile(_url: string, _filename: string): void {
2
+ console.warn("downloadFile: not supported on native");
3
+ }
4
+
5
+ export function downloadBlob(_blob: Blob, _filename: string): void {
6
+ console.warn("downloadBlob: not supported on native");
7
+ }