@multiplatform.one/core 6.7.0 → 7.1.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/package.json +17 -14
- package/src/app/CreateRootLayout.native.tsx +11 -0
- package/src/app/CreateRootLayout.tsx +2 -1
- package/src/app/CreateRootLayout.web.tsx +9 -0
- package/src/app/RuntimePublicConfigScript.spec.tsx +73 -0
- package/src/app/RuntimePublicConfigScript.tsx +54 -0
- package/src/app/deepLinkPath.spec.ts +43 -0
- package/src/app/deepLinkPath.ts +20 -0
- package/src/app/index.ts +1 -0
- package/src/app/rootLayoutShared.ts +9 -0
- package/src/app/useNativeDeepLinks.native.ts +36 -0
- package/src/app/useNativeDeepLinks.ts +8 -0
- package/types/app/CreateRootLayout.d.ts +2 -1
- package/types/app/CreateRootLayout.d.ts.map +1 -1
- package/types/app/CreateRootLayout.native.d.ts.map +1 -1
- package/types/app/CreateRootLayout.web.d.ts.map +1 -1
- package/types/app/RuntimePublicConfigScript.d.ts +35 -0
- package/types/app/RuntimePublicConfigScript.d.ts.map +1 -0
- package/types/app/deepLinkPath.d.ts +16 -0
- package/types/app/deepLinkPath.d.ts.map +1 -0
- package/types/app/index.d.ts +1 -0
- package/types/app/index.d.ts.map +1 -1
- package/types/app/rootLayoutShared.d.ts +9 -0
- package/types/app/rootLayoutShared.d.ts.map +1 -1
- package/types/app/useNativeDeepLinks.d.ts +7 -0
- package/types/app/useNativeDeepLinks.d.ts.map +1 -0
- package/types/app/useNativeDeepLinks.native.d.ts +13 -0
- package/types/app/useNativeDeepLinks.native.d.ts.map +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@multiplatform.one/core",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "7.1.0",
|
|
4
4
|
"description": "Core framework shell, provider composition, and layout factories for multiplatform.one",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"app",
|
|
@@ -46,11 +46,11 @@
|
|
|
46
46
|
"@tanstack/react-form-devtools": "0.2.22",
|
|
47
47
|
"@tanstack/react-pacer-devtools": "^0.7.0",
|
|
48
48
|
"react-cookie": "^8.1.2",
|
|
49
|
-
"@multiplatform.one/
|
|
50
|
-
"@multiplatform.one/
|
|
51
|
-
"@multiplatform.one/
|
|
52
|
-
"@multiplatform.one/
|
|
53
|
-
"@multiplatform.one/
|
|
49
|
+
"@multiplatform.one/i18n": "7.1.0",
|
|
50
|
+
"@multiplatform.one/logger": "7.1.0",
|
|
51
|
+
"@multiplatform.one/platform": "7.1.0",
|
|
52
|
+
"@multiplatform.one/store": "7.1.0",
|
|
53
|
+
"@multiplatform.one/theme": "7.1.0"
|
|
54
54
|
},
|
|
55
55
|
"devDependencies": {
|
|
56
56
|
"@sentry/react": "^10.51.0",
|
|
@@ -60,14 +60,16 @@
|
|
|
60
60
|
"i18next": "^25.10.10",
|
|
61
61
|
"one": "^1.16.5",
|
|
62
62
|
"react": "19.2.5",
|
|
63
|
+
"react-dom": "19.2.5",
|
|
63
64
|
"react-i18next": "^16.6.6",
|
|
64
65
|
"react-native": "0.83.2",
|
|
65
66
|
"react-native-gesture-handler": "~2.31.1",
|
|
66
67
|
"react-native-safe-area-context": "~5.7.0",
|
|
67
|
-
"tamagui": "2.
|
|
68
|
+
"tamagui": "2.7.6",
|
|
68
69
|
"typescript": "~5.9.3",
|
|
69
|
-
"
|
|
70
|
-
"@multiplatform.one/
|
|
70
|
+
"vitest": "^4.1.5",
|
|
71
|
+
"@multiplatform.one/frappe": "7.1.0",
|
|
72
|
+
"@multiplatform.one/web3": "7.1.0"
|
|
71
73
|
},
|
|
72
74
|
"peerDependencies": {
|
|
73
75
|
"@sentry/react": "^9.0.0",
|
|
@@ -83,10 +85,10 @@
|
|
|
83
85
|
"react-native-gesture-handler": ">=2.0.0",
|
|
84
86
|
"react-native-safe-area-context": ">=4.0.0",
|
|
85
87
|
"tamagui": "^2.0.0-rc",
|
|
86
|
-
"@multiplatform.one/frappe": "^
|
|
87
|
-
"@multiplatform.one/
|
|
88
|
-
"@multiplatform.one/
|
|
89
|
-
"@multiplatform.one/
|
|
88
|
+
"@multiplatform.one/frappe": "^7.1.0",
|
|
89
|
+
"@multiplatform.one/frappe-ui": "^7.1.0",
|
|
90
|
+
"@multiplatform.one/keycloak": "^7.1.0",
|
|
91
|
+
"@multiplatform.one/web3": "^7.1.0"
|
|
90
92
|
},
|
|
91
93
|
"peerDependenciesMeta": {
|
|
92
94
|
"@multiplatform.one/web3": {
|
|
@@ -116,6 +118,7 @@
|
|
|
116
118
|
},
|
|
117
119
|
"scripts": {
|
|
118
120
|
"typecheck": "tsgo --noEmit",
|
|
119
|
-
"build": "rm -rf types 2>/dev/null && tsc -p tsconfig.build.json"
|
|
121
|
+
"build": "rm -rf types 2>/dev/null && tsc -p tsconfig.build.json",
|
|
122
|
+
"test": "vitest run"
|
|
120
123
|
}
|
|
121
124
|
}
|
|
@@ -11,6 +11,7 @@ import { config } from "@multiplatform.one/platform";
|
|
|
11
11
|
import { createI18nConfig } from "@multiplatform.one/i18n";
|
|
12
12
|
import { logger } from "@multiplatform.one/logger";
|
|
13
13
|
import type { CreateRootLayoutConfig } from "./rootLayoutShared";
|
|
14
|
+
import { useNativeDeepLinks } from "./useNativeDeepLinks";
|
|
14
15
|
|
|
15
16
|
export type {
|
|
16
17
|
CreateRootLayoutConfig,
|
|
@@ -48,6 +49,15 @@ export function createRootLayout(cfg: CreateRootLayoutConfig): ComponentType {
|
|
|
48
49
|
|
|
49
50
|
const fontsMap = cfg.fonts ?? {};
|
|
50
51
|
|
|
52
|
+
// Rendered inside the mounted tree (after the fonts gate) so the router
|
|
53
|
+
// context is live when the bridge navigates — mirrors the position the
|
|
54
|
+
// consumer-app bridges proved out.
|
|
55
|
+
function DeepLinkBridge() {
|
|
56
|
+
useNativeDeepLinks();
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
DeepLinkBridge.displayName = "DeepLinkBridge";
|
|
60
|
+
|
|
51
61
|
function Layout() {
|
|
52
62
|
const [fontsLoaded, fontError] = useFonts(fontsMap);
|
|
53
63
|
// Gate on fonts being ready, but never block forever: if font loading
|
|
@@ -59,6 +69,7 @@ export function createRootLayout(cfg: CreateRootLayoutConfig): ComponentType {
|
|
|
59
69
|
<SafeAreaProvider>
|
|
60
70
|
<SchemeProvider>
|
|
61
71
|
<NativeRootProvider>
|
|
72
|
+
{cfg.deepLinks !== false && <DeepLinkBridge />}
|
|
62
73
|
<RootLayout>
|
|
63
74
|
<Slot />
|
|
64
75
|
</RootLayout>
|
|
@@ -16,7 +16,8 @@ export type {
|
|
|
16
16
|
} from "./rootLayoutShared";
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
|
-
* Creates a root layout component (universal fallback for
|
|
19
|
+
* Creates a root layout component (universal fallback for non-web hosts —
|
|
20
|
+
* Tauri, GNOME, extensions).
|
|
20
21
|
*
|
|
21
22
|
* Handles i18n initialization, color-scheme detection via `matchMedia`,
|
|
22
23
|
* and wraps content in `LoadProgressBar` → `AppProvider` → `RootLayout` → `<Slot />`.
|
|
@@ -8,6 +8,7 @@ import { config, isDev, platform } from "@multiplatform.one/platform";
|
|
|
8
8
|
import { createI18nConfig } from "@multiplatform.one/i18n";
|
|
9
9
|
import { logger } from "@multiplatform.one/logger";
|
|
10
10
|
import { storage } from "@multiplatform.one/store";
|
|
11
|
+
import { RuntimePublicConfigScript } from "./RuntimePublicConfigScript";
|
|
11
12
|
import type { CreateRootLayoutConfig, RootLoaderData } from "./rootLayoutShared";
|
|
12
13
|
|
|
13
14
|
export type {
|
|
@@ -142,6 +143,14 @@ export function createRootLayout(cfg: CreateRootLayoutConfig): ComponentType {
|
|
|
142
143
|
<html lang={htmlLang}>
|
|
143
144
|
<head>
|
|
144
145
|
<meta charSet="utf-8" />
|
|
146
|
+
{/* RUNTIME public config, first thing after charset so it is parsed
|
|
147
|
+
and executed before the deferred app bundle constructs Config.
|
|
148
|
+
Without it the browser has NO path to config at all: Config's env
|
|
149
|
+
lookup reads process.env, which does not exist there, so a
|
|
150
|
+
container env var (MARKETPLACE_API_URL, FRAPPE_ENABLED, …) could
|
|
151
|
+
never reach the SPA and the image had to be rebuilt per
|
|
152
|
+
environment. See RuntimePublicConfigScript. */}
|
|
153
|
+
<RuntimePublicConfigScript />
|
|
145
154
|
{interFonts && (
|
|
146
155
|
<>
|
|
147
156
|
<link
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { runtimePublicConfigKey } from "@multiplatform.one/platform";
|
|
5
|
+
import { renderToStaticMarkup } from "react-dom/server";
|
|
6
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
7
|
+
import { RuntimePublicConfigScript } from "./RuntimePublicConfigScript";
|
|
8
|
+
|
|
9
|
+
// vitest runs this suite in the node environment, so `window` is undefined and
|
|
10
|
+
// `platform.isServer` is true — i.e. every render below is the SSR branch, the
|
|
11
|
+
// one that actually writes the served document.
|
|
12
|
+
function renderDocument(): string {
|
|
13
|
+
return renderToStaticMarkup(<RuntimePublicConfigScript />);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
vi.unstubAllEnvs();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
describe("RuntimePublicConfigScript", () => {
|
|
21
|
+
it("emits the public payload into the SSR document", () => {
|
|
22
|
+
vi.stubEnv("VITE_MP_PUBLIC_CONFIG_KEYS", JSON.stringify(["MARKETPLACE_API_URL"]));
|
|
23
|
+
vi.stubEnv("MARKETPLACE_API_URL", "https://api.marketplace.example.org");
|
|
24
|
+
const html = renderDocument();
|
|
25
|
+
// The whole point of the seam: the VALUE the SSR process holds in its own
|
|
26
|
+
// env at request time is in the bytes the browser receives. Nothing about
|
|
27
|
+
// this depends on what the image was BUILT with.
|
|
28
|
+
expect(html).toContain("<script");
|
|
29
|
+
expect(html).toContain(runtimePublicConfigKey);
|
|
30
|
+
expect(html).toContain("https://api.marketplace.example.org");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("emits FRAPPE_ENABLED so the frappe provider can mount in the browser", () => {
|
|
34
|
+
// CreateApp.tsx: `cfg.enabled ?? config.get("FRAPPE_ENABLED") === "1"`.
|
|
35
|
+
vi.stubEnv("VITE_MP_PUBLIC_CONFIG_KEYS", JSON.stringify(["FRAPPE_ENABLED"]));
|
|
36
|
+
vi.stubEnv("FRAPPE_ENABLED", "1");
|
|
37
|
+
expect(renderDocument()).toContain('"FRAPPE_ENABLED":"1"');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("keeps a private key out of the SSR document", () => {
|
|
41
|
+
vi.stubEnv("VITE_MP_PUBLIC_CONFIG_KEYS", JSON.stringify(["MARKETPLACE_API_URL"]));
|
|
42
|
+
vi.stubEnv("MARKETPLACE_API_URL", "https://api.marketplace.example.org");
|
|
43
|
+
vi.stubEnv("SECRET", "hunter2");
|
|
44
|
+
vi.stubEnv("DATABASE_PASSWORD", "hunter2");
|
|
45
|
+
const html = renderDocument();
|
|
46
|
+
expect(html).not.toContain("SECRET");
|
|
47
|
+
expect(html).not.toContain("DATABASE_PASSWORD");
|
|
48
|
+
expect(html).not.toContain("hunter2");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("cannot be closed out of by a value that looks like markup", () => {
|
|
52
|
+
vi.stubEnv("VITE_MP_PUBLIC_CONFIG_KEYS", JSON.stringify(["BASE_URL"]));
|
|
53
|
+
vi.stubEnv("BASE_URL", "</script><script>alert(1)</script>");
|
|
54
|
+
const html = renderDocument();
|
|
55
|
+
expect(html).not.toContain("</script><script>");
|
|
56
|
+
expect(html).toContain("\\u003c");
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe("createRootLayout (web)", () => {
|
|
61
|
+
it("mounts RuntimePublicConfigScript in the document head", () => {
|
|
62
|
+
// The component only reaches a browser if the root layout renders it, and
|
|
63
|
+
// the root layout is a `one` route module that cannot be instantiated
|
|
64
|
+
// outside a router — so the mount is pinned at the source level. Delete
|
|
65
|
+
// the element from <head> and this reds.
|
|
66
|
+
const source = readFileSync(
|
|
67
|
+
join(dirname(fileURLToPath(import.meta.url)), "CreateRootLayout.web.tsx"),
|
|
68
|
+
"utf-8",
|
|
69
|
+
);
|
|
70
|
+
const head = source.slice(source.indexOf("<head>"), source.indexOf("</head>"));
|
|
71
|
+
expect(head).toContain("<RuntimePublicConfigScript />");
|
|
72
|
+
});
|
|
73
|
+
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import {
|
|
2
|
+
platform,
|
|
3
|
+
runtimePublicConfigKey,
|
|
4
|
+
serializeRuntimePublicConfig,
|
|
5
|
+
} from "@multiplatform.one/platform";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Publishes the SSR-resolved PUBLIC config into the served document.
|
|
9
|
+
*
|
|
10
|
+
* Mounted in `<head>` by `createRootLayout` (web). Deliberately the SAME shape
|
|
11
|
+
* one uses for `__one_server_context__` — an inline `<script>` assigning a
|
|
12
|
+
* `globalThis` key, `async` + `href` so React 19 keys it stably, and
|
|
13
|
+
* `suppressHydrationWarning` because the client renders an EMPTY body for the
|
|
14
|
+
* already-executed script (one's `server/ServerContextScript` does exactly
|
|
15
|
+
* this). It is a sibling payload on the established seam, not a second
|
|
16
|
+
* transport.
|
|
17
|
+
*
|
|
18
|
+
* XSS: the injected string is NOT user content. It is built by
|
|
19
|
+
* `serializeRuntimePublicConfig`, which (a) walks the BUILD-TIME public key
|
|
20
|
+
* allowlist rather than any request-derived input, and (b) emits the values
|
|
21
|
+
* through a JSON serializer that escapes `<`, `>`, U+2028 and U+2029 — so the
|
|
22
|
+
* body cannot close its own `</script>` tag or terminate a JS statement. This
|
|
23
|
+
* is the same sanitizer shape one ships as `utils/htmlEscape.safeJsonStringify`
|
|
24
|
+
* for the server-context payload. React offers no other way to emit a script
|
|
25
|
+
* body: text children of `<script>` are HTML-escaped, which would corrupt the
|
|
26
|
+
* JSON quoting.
|
|
27
|
+
*
|
|
28
|
+
* Ordering: an inline classic script in `<head>` runs during document parse,
|
|
29
|
+
* while the app bundle is a deferred module script — so `globalThis` already
|
|
30
|
+
* carries the payload by the time `@multiplatform.one/platform` constructs its
|
|
31
|
+
* `Config`.
|
|
32
|
+
*
|
|
33
|
+
* On the browser the body is empty: `serializeRuntimePublicConfig()` reads
|
|
34
|
+
* `process.env`, which only exists on the server. Rendering it client-side
|
|
35
|
+
* would publish an EMPTY payload over the real one.
|
|
36
|
+
*/
|
|
37
|
+
export function RuntimePublicConfigScript() {
|
|
38
|
+
return (
|
|
39
|
+
<script
|
|
40
|
+
async
|
|
41
|
+
// @ts-expect-error `href` is not on React's ScriptHTMLAttributes — React 19
|
|
42
|
+
// keys hoisted scripts by it anyway, and `one`'s ServerContextScript (the
|
|
43
|
+
// sibling payload this deliberately mirrors) carries the identical
|
|
44
|
+
// suppression on the identical line. Copying the transport means copying
|
|
45
|
+
// this, not inventing a differently-keyed script.
|
|
46
|
+
href={runtimePublicConfigKey}
|
|
47
|
+
suppressHydrationWarning
|
|
48
|
+
dangerouslySetInnerHTML={{
|
|
49
|
+
__html: platform.isServer ? serializeRuntimePublicConfig() : "",
|
|
50
|
+
}}
|
|
51
|
+
/>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
RuntimePublicConfigScript.displayName = "RuntimePublicConfigScript";
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @vitest-environment node
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, expect, it } from "vitest";
|
|
6
|
+
import { deepLinkUrlToPath } from "./deepLinkPath";
|
|
7
|
+
|
|
8
|
+
describe("deepLinkUrlToPath", () => {
|
|
9
|
+
it("maps a scheme host to the first path segment", () => {
|
|
10
|
+
expect(deepLinkUrlToPath("myapp://pos")).toBe("/pos");
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("keeps nested paths and query strings", () => {
|
|
14
|
+
expect(deepLinkUrlToPath("myapp://order/123?tab=items")).toBe("/order/123?tab=items");
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("strips trailing slashes", () => {
|
|
18
|
+
expect(deepLinkUrlToPath("myapp://kds/")).toBe("/kds");
|
|
19
|
+
expect(deepLinkUrlToPath("myapp://kds///")).toBe("/kds");
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("supports dotted/plus/hyphen scheme names", () => {
|
|
23
|
+
expect(deepLinkUrlToPath("com.example.app://board")).toBe("/board");
|
|
24
|
+
expect(deepLinkUrlToPath("my-app+beta://menu")).toBe("/menu");
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("handles the scheme://host/path shape from https-style links", () => {
|
|
28
|
+
expect(deepLinkUrlToPath("https://example.com/admin")).toBe("/example.com/admin");
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("returns null for root-only or empty URLs (no-op, keep current route)", () => {
|
|
32
|
+
expect(deepLinkUrlToPath("myapp://")).toBeNull();
|
|
33
|
+
expect(deepLinkUrlToPath("myapp:///")).toBeNull();
|
|
34
|
+
expect(deepLinkUrlToPath("")).toBeNull();
|
|
35
|
+
expect(deepLinkUrlToPath(null)).toBeNull();
|
|
36
|
+
expect(deepLinkUrlToPath(undefined)).toBeNull();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("returns null for non-URL strings", () => {
|
|
40
|
+
expect(deepLinkUrlToPath("not a url")).toBeNull();
|
|
41
|
+
expect(deepLinkUrlToPath("/already/a/path")).toBeNull();
|
|
42
|
+
});
|
|
43
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure deep-link URL → router path mapping (platform-free, unit-tested).
|
|
3
|
+
*
|
|
4
|
+
* One's built-in NavigationContainer linking never applies scheme URLs on
|
|
5
|
+
* native — the router is always initialized with location "/" (one's
|
|
6
|
+
* Root.native pins initialLocation, ignoring the launch URL) and warm url
|
|
7
|
+
* events don't dispatch — so OS URLs are mapped to router navigation by the
|
|
8
|
+
* native root layout instead (see useNativeDeepLinks).
|
|
9
|
+
*
|
|
10
|
+
* `scheme://host/path?query` → `/host/path?query` (the scheme's "host" is
|
|
11
|
+
* the first path segment: `myapp://pos` → `/pos`). Returns null for empty /
|
|
12
|
+
* root-only URLs so callers can no-op instead of clobbering the current
|
|
13
|
+
* route.
|
|
14
|
+
*/
|
|
15
|
+
export function deepLinkUrlToPath(url: string | null | undefined): string | null {
|
|
16
|
+
const rest = url?.match(/^[\w.+-]+:\/\/+(.*)$/)?.[1];
|
|
17
|
+
if (!rest) return null;
|
|
18
|
+
const path = `/${rest.replace(/\/+$/, "")}`;
|
|
19
|
+
return path === "/" ? null : path;
|
|
20
|
+
}
|
package/src/app/index.ts
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
* `.ios.tsx` > `.native.tsx` > `.tsx` (on iOS)
|
|
20
20
|
* `.web.tsx` > `.tsx` (on web)
|
|
21
21
|
* `.tauri.tsx` > `.tsx` (on Tauri)
|
|
22
|
+
* `.native.tsx` > `.tsx` (on GNOME — see apps/one/vite.config.gnome.ts)
|
|
22
23
|
*
|
|
23
24
|
* When creating platform-specific variants, always keep a universal
|
|
24
25
|
* fallback file (e.g. `index.ts`) that exports the default API.
|
|
@@ -68,4 +68,13 @@ export interface CreateRootLayoutConfig {
|
|
|
68
68
|
* Ignored on web (fonts are loaded via CSS side-effect imports).
|
|
69
69
|
*/
|
|
70
70
|
fonts?: Record<string, number>;
|
|
71
|
+
/**
|
|
72
|
+
* Native deep-link bridge (default true; ignored on web). One's built-in
|
|
73
|
+
* NavigationContainer linking never applies scheme URLs on native (the
|
|
74
|
+
* Root pins initialLocation to "/" and warm url events don't dispatch), so
|
|
75
|
+
* the root layout maps them itself: cold-start launch URL →
|
|
76
|
+
* `router.replace`, warm url events → `router.push`
|
|
77
|
+
* (`myapp://pos` → `/pos`). Set false for apps that own their own linking.
|
|
78
|
+
*/
|
|
79
|
+
deepLinks?: boolean;
|
|
71
80
|
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { useEffect } from "react";
|
|
2
|
+
import { Linking } from "react-native";
|
|
3
|
+
import type { Href } from "one";
|
|
4
|
+
import { useRouter } from "one";
|
|
5
|
+
import { deepLinkUrlToPath } from "./deepLinkPath";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Deep-link bridge (native): One's built-in NavigationContainer linking
|
|
9
|
+
* never applies scheme URLs — the router is always initialized with
|
|
10
|
+
* location "/" (one's Root.native pins initialLocation, so the launch URL
|
|
11
|
+
* is ignored) and warm url events never dispatch. Map OS URLs to router
|
|
12
|
+
* navigation instead: the cold-start launch URL replaces (so back doesn't
|
|
13
|
+
* land on the ignored "/") and warm url events push.
|
|
14
|
+
*
|
|
15
|
+
* Wired by createRootLayout's native Layout; opt out with
|
|
16
|
+
* `createRootLayout({ deepLinks: false, ... })`.
|
|
17
|
+
*/
|
|
18
|
+
export function useNativeDeepLinks(): void {
|
|
19
|
+
const router = useRouter();
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
let cancelled = false;
|
|
22
|
+
Linking.getInitialURL().then((url) => {
|
|
23
|
+
if (cancelled) return;
|
|
24
|
+
const path = deepLinkUrlToPath(url);
|
|
25
|
+
if (path) router.replace(path as Href);
|
|
26
|
+
});
|
|
27
|
+
const subscription = Linking.addEventListener("url", ({ url }) => {
|
|
28
|
+
const path = deepLinkUrlToPath(url);
|
|
29
|
+
if (path) router.push(path as Href);
|
|
30
|
+
});
|
|
31
|
+
return () => {
|
|
32
|
+
cancelled = true;
|
|
33
|
+
subscription.remove();
|
|
34
|
+
};
|
|
35
|
+
}, [router]);
|
|
36
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Web / type-resolution twin of useNativeDeepLinks.native.ts — deep links on
|
|
3
|
+
* web are just URLs, the browser and One's web router already apply them.
|
|
4
|
+
* Only the `.native` sibling does anything.
|
|
5
|
+
*/
|
|
6
|
+
export function useNativeDeepLinks(): void {
|
|
7
|
+
// no-op on web
|
|
8
|
+
}
|
|
@@ -2,7 +2,8 @@ import { type ComponentType } from "react";
|
|
|
2
2
|
import type { CreateRootLayoutConfig } from "./rootLayoutShared";
|
|
3
3
|
export type { CreateRootLayoutConfig, HeadConfig, RootLayoutProps, RootLoaderData, SentryConfig, } from "./rootLayoutShared";
|
|
4
4
|
/**
|
|
5
|
-
* Creates a root layout component (universal fallback for
|
|
5
|
+
* Creates a root layout component (universal fallback for non-web hosts —
|
|
6
|
+
* Tauri, GNOME, extensions).
|
|
6
7
|
*
|
|
7
8
|
* Handles i18n initialization, color-scheme detection via `matchMedia`,
|
|
8
9
|
* and wraps content in `LoadProgressBar` → `AppProvider` → `RootLayout` → `<Slot />`.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CreateRootLayout.d.ts","sourceRoot":"","sources":["../../src/app/CreateRootLayout.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAuB,KAAK,aAAa,EAAkB,MAAM,OAAO,CAAC;AAKhF,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,oBAAoB,CAAC;AAEjE,YAAY,EACV,sBAAsB,EACtB,UAAU,EACV,eAAe,EACf,cAAc,EACd,YAAY,GACb,MAAM,oBAAoB,CAAC;AAE5B
|
|
1
|
+
{"version":3,"file":"CreateRootLayout.d.ts","sourceRoot":"","sources":["../../src/app/CreateRootLayout.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAuB,KAAK,aAAa,EAAkB,MAAM,OAAO,CAAC;AAKhF,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,oBAAoB,CAAC;AAEjE,YAAY,EACV,sBAAsB,EACtB,UAAU,EACV,eAAe,EACf,cAAc,EACd,YAAY,GACb,MAAM,oBAAoB,CAAC;AAE5B;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,sBAAsB,GAAG,aAAa,CAwC3E"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CreateRootLayout.native.d.ts","sourceRoot":"","sources":["../../src/app/CreateRootLayout.native.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,aAAa,EAAa,MAAM,OAAO,CAAC;AAQtD,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"CreateRootLayout.native.d.ts","sourceRoot":"","sources":["../../src/app/CreateRootLayout.native.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,aAAa,EAAa,MAAM,OAAO,CAAC;AAQtD,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,oBAAoB,CAAC;AAGjE,YAAY,EACV,sBAAsB,EACtB,UAAU,EACV,eAAe,EACf,cAAc,EACd,YAAY,GACb,MAAM,oBAAoB,CAAC;AAM5B;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,sBAAsB,GAAG,aAAa,CAgD3E"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CreateRootLayout.web.d.ts","sourceRoot":"","sources":["../../src/app/CreateRootLayout.web.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,EAAa,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"CreateRootLayout.web.d.ts","sourceRoot":"","sources":["../../src/app/CreateRootLayout.web.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,EAAa,MAAM,OAAO,CAAC;AAQtD,OAAO,KAAK,EAAE,sBAAsB,EAAkB,MAAM,oBAAoB,CAAC;AAEjF,YAAY,EACV,sBAAsB,EACtB,UAAU,EACV,eAAe,EACf,cAAc,EACd,YAAY,GACb,MAAM,oBAAoB,CAAC;AAE5B;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,sBAAsB,GAAG,aAAa,CA4L3E"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Publishes the SSR-resolved PUBLIC config into the served document.
|
|
3
|
+
*
|
|
4
|
+
* Mounted in `<head>` by `createRootLayout` (web). Deliberately the SAME shape
|
|
5
|
+
* one uses for `__one_server_context__` — an inline `<script>` assigning a
|
|
6
|
+
* `globalThis` key, `async` + `href` so React 19 keys it stably, and
|
|
7
|
+
* `suppressHydrationWarning` because the client renders an EMPTY body for the
|
|
8
|
+
* already-executed script (one's `server/ServerContextScript` does exactly
|
|
9
|
+
* this). It is a sibling payload on the established seam, not a second
|
|
10
|
+
* transport.
|
|
11
|
+
*
|
|
12
|
+
* XSS: the injected string is NOT user content. It is built by
|
|
13
|
+
* `serializeRuntimePublicConfig`, which (a) walks the BUILD-TIME public key
|
|
14
|
+
* allowlist rather than any request-derived input, and (b) emits the values
|
|
15
|
+
* through a JSON serializer that escapes `<`, `>`, U+2028 and U+2029 — so the
|
|
16
|
+
* body cannot close its own `</script>` tag or terminate a JS statement. This
|
|
17
|
+
* is the same sanitizer shape one ships as `utils/htmlEscape.safeJsonStringify`
|
|
18
|
+
* for the server-context payload. React offers no other way to emit a script
|
|
19
|
+
* body: text children of `<script>` are HTML-escaped, which would corrupt the
|
|
20
|
+
* JSON quoting.
|
|
21
|
+
*
|
|
22
|
+
* Ordering: an inline classic script in `<head>` runs during document parse,
|
|
23
|
+
* while the app bundle is a deferred module script — so `globalThis` already
|
|
24
|
+
* carries the payload by the time `@multiplatform.one/platform` constructs its
|
|
25
|
+
* `Config`.
|
|
26
|
+
*
|
|
27
|
+
* On the browser the body is empty: `serializeRuntimePublicConfig()` reads
|
|
28
|
+
* `process.env`, which only exists on the server. Rendering it client-side
|
|
29
|
+
* would publish an EMPTY payload over the real one.
|
|
30
|
+
*/
|
|
31
|
+
export declare function RuntimePublicConfigScript(): import("react/jsx-runtime").JSX.Element;
|
|
32
|
+
export declare namespace RuntimePublicConfigScript {
|
|
33
|
+
var displayName: string;
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=RuntimePublicConfigScript.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"RuntimePublicConfigScript.d.ts","sourceRoot":"","sources":["../../src/app/RuntimePublicConfigScript.tsx"],"names":[],"mappings":"AAMA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAgB,yBAAyB,4CAgBxC;yBAhBe,yBAAyB"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure deep-link URL → router path mapping (platform-free, unit-tested).
|
|
3
|
+
*
|
|
4
|
+
* One's built-in NavigationContainer linking never applies scheme URLs on
|
|
5
|
+
* native — the router is always initialized with location "/" (one's
|
|
6
|
+
* Root.native pins initialLocation, ignoring the launch URL) and warm url
|
|
7
|
+
* events don't dispatch — so OS URLs are mapped to router navigation by the
|
|
8
|
+
* native root layout instead (see useNativeDeepLinks).
|
|
9
|
+
*
|
|
10
|
+
* `scheme://host/path?query` → `/host/path?query` (the scheme's "host" is
|
|
11
|
+
* the first path segment: `myapp://pos` → `/pos`). Returns null for empty /
|
|
12
|
+
* root-only URLs so callers can no-op instead of clobbering the current
|
|
13
|
+
* route.
|
|
14
|
+
*/
|
|
15
|
+
export declare function deepLinkUrlToPath(url: string | null | undefined): string | null;
|
|
16
|
+
//# sourceMappingURL=deepLinkPath.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"deepLinkPath.d.ts","sourceRoot":"","sources":["../../src/app/deepLinkPath.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,GAAG,IAAI,CAK/E"}
|
package/types/app/index.d.ts
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
* `.ios.tsx` > `.native.tsx` > `.tsx` (on iOS)
|
|
20
20
|
* `.web.tsx` > `.tsx` (on web)
|
|
21
21
|
* `.tauri.tsx` > `.tsx` (on Tauri)
|
|
22
|
+
* `.native.tsx` > `.tsx` (on GNOME — see apps/one/vite.config.gnome.ts)
|
|
22
23
|
*
|
|
23
24
|
* When creating platform-specific variants, always keep a universal
|
|
24
25
|
* fallback file (e.g. `index.ts`) that exports the default API.
|
package/types/app/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/app/index.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/app/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,cAAc,aAAa,CAAC;AAC5B,cAAc,oBAAoB,CAAC"}
|
|
@@ -63,5 +63,14 @@ export interface CreateRootLayoutConfig {
|
|
|
63
63
|
* Ignored on web (fonts are loaded via CSS side-effect imports).
|
|
64
64
|
*/
|
|
65
65
|
fonts?: Record<string, number>;
|
|
66
|
+
/**
|
|
67
|
+
* Native deep-link bridge (default true; ignored on web). One's built-in
|
|
68
|
+
* NavigationContainer linking never applies scheme URLs on native (the
|
|
69
|
+
* Root pins initialLocation to "/" and warm url events don't dispatch), so
|
|
70
|
+
* the root layout maps them itself: cold-start launch URL →
|
|
71
|
+
* `router.replace`, warm url events → `router.push`
|
|
72
|
+
* (`myapp://pos` → `/pos`). Set false for apps that own their own linking.
|
|
73
|
+
*/
|
|
74
|
+
deepLinks?: boolean;
|
|
66
75
|
}
|
|
67
76
|
//# sourceMappingURL=rootLayoutShared.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rootLayoutShared.d.ts","sourceRoot":"","sources":["../../src/app/rootLayoutShared.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,OAAO,CAAC;AAC9D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AACjE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAEpD,MAAM,WAAW,UAAU;IACzB,6CAA6C;IAC7C,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;;;;;;OASG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAED,MAAM,WAAW,YAAY;IAC3B,0EAA0E;IAC1E,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,qEAAqE;AACrE,MAAM,WAAW,cAAc;IAC7B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,yEAAyE;IACzE,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,yEAAyE;IACzE,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED;;;;;;GAMG;AACH,MAAM,WAAW,eAAgB,SAAQ,iBAAiB;IACxD,6DAA6D;IAC7D,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAED;;;;;GAKG;AACH,MAAM,WAAW,sBAAsB;IACrC,6DAA6D;IAC7D,WAAW,EAAE,aAAa,CAAC,gBAAgB,CAAC,CAAC;IAC7C,oFAAoF;IACpF,UAAU,EAAE,aAAa,CAAC,eAAe,CAAC,CAAC;IAC3C,8FAA8F;IAC9F,IAAI,EAAE,iBAAiB,CAAC;IACxB,qDAAqD;IACrD,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB,oFAAoF;IACpF,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"rootLayoutShared.d.ts","sourceRoot":"","sources":["../../src/app/rootLayoutShared.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,OAAO,CAAC;AAC9D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AACjE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAEpD,MAAM,WAAW,UAAU;IACzB,6CAA6C;IAC7C,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;;;;;;OASG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAED,MAAM,WAAW,YAAY;IAC3B,0EAA0E;IAC1E,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,qEAAqE;AACrE,MAAM,WAAW,cAAc;IAC7B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,yEAAyE;IACzE,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,yEAAyE;IACzE,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED;;;;;;GAMG;AACH,MAAM,WAAW,eAAgB,SAAQ,iBAAiB;IACxD,6DAA6D;IAC7D,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAED;;;;;GAKG;AACH,MAAM,WAAW,sBAAsB;IACrC,6DAA6D;IAC7D,WAAW,EAAE,aAAa,CAAC,gBAAgB,CAAC,CAAC;IAC7C,oFAAoF;IACpF,UAAU,EAAE,aAAa,CAAC,eAAe,CAAC,CAAC;IAC3C,8FAA8F;IAC9F,IAAI,EAAE,iBAAiB,CAAC;IACxB,qDAAqD;IACrD,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB,oFAAoF;IACpF,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B;;;;;;;OAOG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Web / type-resolution twin of useNativeDeepLinks.native.ts — deep links on
|
|
3
|
+
* web are just URLs, the browser and One's web router already apply them.
|
|
4
|
+
* Only the `.native` sibling does anything.
|
|
5
|
+
*/
|
|
6
|
+
export declare function useNativeDeepLinks(): void;
|
|
7
|
+
//# sourceMappingURL=useNativeDeepLinks.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useNativeDeepLinks.d.ts","sourceRoot":"","sources":["../../src/app/useNativeDeepLinks.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,wBAAgB,kBAAkB,IAAI,IAAI,CAEzC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deep-link bridge (native): One's built-in NavigationContainer linking
|
|
3
|
+
* never applies scheme URLs — the router is always initialized with
|
|
4
|
+
* location "/" (one's Root.native pins initialLocation, so the launch URL
|
|
5
|
+
* is ignored) and warm url events never dispatch. Map OS URLs to router
|
|
6
|
+
* navigation instead: the cold-start launch URL replaces (so back doesn't
|
|
7
|
+
* land on the ignored "/") and warm url events push.
|
|
8
|
+
*
|
|
9
|
+
* Wired by createRootLayout's native Layout; opt out with
|
|
10
|
+
* `createRootLayout({ deepLinks: false, ... })`.
|
|
11
|
+
*/
|
|
12
|
+
export declare function useNativeDeepLinks(): void;
|
|
13
|
+
//# sourceMappingURL=useNativeDeepLinks.native.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useNativeDeepLinks.native.d.ts","sourceRoot":"","sources":["../../src/app/useNativeDeepLinks.native.ts"],"names":[],"mappings":"AAMA;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,IAAI,IAAI,CAkBzC"}
|