@multiplatform.one/core 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,74 @@
1
+ import { SchemeProvider, useUserScheme } from "@vxrn/color-scheme";
2
+ import { useFonts } from "expo-font";
3
+ import i18n from "i18next";
4
+ import { Slot } from "one";
5
+ import type { ComponentType, ReactNode } from "react";
6
+ import { StyleSheet } from "react-native";
7
+ import { GestureHandlerRootView } from "react-native-gesture-handler";
8
+ import { SafeAreaProvider } from "react-native-safe-area-context";
9
+ import { initReactI18next } from "react-i18next";
10
+ import { config } from "@multiplatform.one/platform";
11
+ import { createI18nConfig } from "@multiplatform.one/i18n";
12
+ import { logger } from "@multiplatform.one/logger";
13
+ import type { CreateRootLayoutConfig } from "./rootLayoutShared";
14
+
15
+ export type {
16
+ CreateRootLayoutConfig,
17
+ HeadConfig,
18
+ RootLayoutProps,
19
+ RootLoaderData,
20
+ SentryConfig,
21
+ } from "./rootLayoutShared";
22
+
23
+ const styles = StyleSheet.create({
24
+ container: { flex: 1 },
25
+ });
26
+
27
+ /**
28
+ * Creates a root layout component for **React Native** (iOS + Android).
29
+ *
30
+ * Handles i18n initialization, color-scheme detection via `@vxrn/color-scheme`,
31
+ * and wraps content in `GestureHandlerRootView` → `SafeAreaProvider` →
32
+ * `SchemeProvider` → `AppProvider` → `RootLayout` → `<Slot />`.
33
+ *
34
+ * `head` and `sentry` config options are ignored on native.
35
+ */
36
+ export function createRootLayout(cfg: CreateRootLayoutConfig): ComponentType {
37
+ i18n.use(initReactI18next).init(createI18nConfig(cfg.i18n)).catch(logger.error);
38
+ i18n.changeLanguage(config.get("I18N_DEFAULT_LANGUAGE", cfg.i18n.defaultLanguage));
39
+
40
+ const { AppProvider, RootLayout } = cfg;
41
+
42
+ function NativeRootProvider({ children }: { children: ReactNode }) {
43
+ const { value: systemScheme } = useUserScheme();
44
+
45
+ return <AppProvider systemTheme={systemScheme}>{children}</AppProvider>;
46
+ }
47
+ NativeRootProvider.displayName = "NativeRootProvider";
48
+
49
+ const fontsMap = cfg.fonts ?? {};
50
+
51
+ function Layout() {
52
+ const [fontsLoaded, fontError] = useFonts(fontsMap);
53
+ // Gate on fonts being ready, but never block forever: if font loading
54
+ // fails (e.g. asset fetch errors in a dev client), still render with the
55
+ // system fallback rather than leaving a permanently blank screen.
56
+ if (!fontsLoaded && !fontError) return null;
57
+ return (
58
+ <GestureHandlerRootView style={styles.container}>
59
+ <SafeAreaProvider>
60
+ <SchemeProvider>
61
+ <NativeRootProvider>
62
+ <RootLayout>
63
+ <Slot />
64
+ </RootLayout>
65
+ </NativeRootProvider>
66
+ </SchemeProvider>
67
+ </SafeAreaProvider>
68
+ </GestureHandlerRootView>
69
+ );
70
+ }
71
+ Layout.displayName = "RootLayout";
72
+
73
+ return Layout;
74
+ }
@@ -0,0 +1,66 @@
1
+ import i18n from "i18next";
2
+ import { LoadProgressBar, Slot } from "one";
3
+ import { useEffect, useState, type ComponentType, type ReactNode } from "react";
4
+ import { initReactI18next } from "react-i18next";
5
+ import { config } from "@multiplatform.one/platform";
6
+ import { createI18nConfig } from "@multiplatform.one/i18n";
7
+ import { logger } from "@multiplatform.one/logger";
8
+ import type { CreateRootLayoutConfig } from "./rootLayoutShared";
9
+
10
+ export type {
11
+ CreateRootLayoutConfig,
12
+ HeadConfig,
13
+ RootLayoutProps,
14
+ RootLoaderData,
15
+ SentryConfig,
16
+ } from "./rootLayoutShared";
17
+
18
+ /**
19
+ * Creates a root layout component (universal fallback for Tauri / desktop).
20
+ *
21
+ * Handles i18n initialization, color-scheme detection via `matchMedia`,
22
+ * and wraps content in `LoadProgressBar` → `AppProvider` → `RootLayout` → `<Slot />`.
23
+ *
24
+ * `head` and `sentry` config options are ignored in the universal fallback.
25
+ */
26
+ export function createRootLayout(cfg: CreateRootLayoutConfig): ComponentType {
27
+ i18n.use(initReactI18next).init(createI18nConfig(cfg.i18n)).catch(logger.error);
28
+ i18n.changeLanguage(config.get("I18N_DEFAULT_LANGUAGE", cfg.i18n.defaultLanguage));
29
+
30
+ const { AppProvider, RootLayout } = cfg;
31
+
32
+ function RootProvider({ children }: { children: ReactNode }) {
33
+ const [scheme, setScheme] = useState<"light" | "dark">("light");
34
+
35
+ useEffect(() => {
36
+ if (typeof window !== "undefined" && window.matchMedia) {
37
+ const darkModeQuery = window.matchMedia("(prefers-color-scheme: dark)");
38
+ setScheme(darkModeQuery.matches ? "dark" : "light");
39
+ const handleChange = (e: MediaQueryListEvent) => {
40
+ setScheme(e.matches ? "dark" : "light");
41
+ };
42
+ darkModeQuery.addEventListener("change", handleChange);
43
+ return () => darkModeQuery.removeEventListener("change", handleChange);
44
+ }
45
+ }, []);
46
+
47
+ return <AppProvider systemTheme={scheme}>{children}</AppProvider>;
48
+ }
49
+ RootProvider.displayName = "RootProvider";
50
+
51
+ function Layout() {
52
+ return (
53
+ <>
54
+ <LoadProgressBar />
55
+ <RootProvider>
56
+ <RootLayout>
57
+ <Slot />
58
+ </RootLayout>
59
+ </RootProvider>
60
+ </>
61
+ );
62
+ }
63
+ Layout.displayName = "RootLayout";
64
+
65
+ return Layout;
66
+ }
@@ -0,0 +1,181 @@
1
+ import i18n from "i18next";
2
+ import { LoadProgressBar, Slot, useMatches } from "one";
3
+ import { SchemeProvider, setUserScheme, useUserScheme } from "@vxrn/color-scheme";
4
+ import type { ComponentType, ReactNode } from "react";
5
+ import { CookiesProvider, Cookies } from "react-cookie";
6
+ import { initReactI18next } from "react-i18next";
7
+ import { config, isDev, platform } from "@multiplatform.one/platform";
8
+ import { createI18nConfig } from "@multiplatform.one/i18n";
9
+ import { logger } from "@multiplatform.one/logger";
10
+ import { storage } from "@multiplatform.one/store";
11
+ import type { CreateRootLayoutConfig, RootLoaderData } from "./rootLayoutShared";
12
+
13
+ export type {
14
+ CreateRootLayoutConfig,
15
+ HeadConfig,
16
+ RootLayoutProps,
17
+ RootLoaderData,
18
+ SentryConfig,
19
+ } from "./rootLayoutShared";
20
+
21
+ /**
22
+ * Creates a root layout component for **web** (browser + SSR).
23
+ *
24
+ * Handles i18n initialization, Sentry setup, color-scheme detection via
25
+ * `SchemeProvider` (blocking script + localStorage – no cookie needed),
26
+ * `<Head>` meta tags, `<LoadProgressBar>`, and wraps content in
27
+ * `SchemeProvider` → `CookiesProvider` → `AppProvider` → `RootLayout` → `<Slot />`.
28
+ *
29
+ * Light/dark scheme: handled by `@vxrn/color-scheme` `SchemeProvider` which
30
+ * injects a blocking `<script>` that reads `localStorage` / `matchMedia`
31
+ * before the first paint. Works even when cookies are disabled.
32
+ *
33
+ * Theme properties (color theme, padding, borderRadius, …): synced via
34
+ * `mp.preset` + `mp.overrides` cookies so SSR can render with the user's full
35
+ * theme. The cookies are populated by `useTheme` in `@multiplatform.one/theme`.
36
+ */
37
+ export function createRootLayout(cfg: CreateRootLayoutConfig): ComponentType {
38
+ // Eagerly patch the @vxrn/color-scheme module state BEFORE React renders.
39
+ //
40
+ // On web, useUserScheme() initializes with { value: 'light' } for SSR
41
+ // compatibility. This means SchemeProvider's layout effect briefly sets
42
+ // `t_light` on <html>, overriding the correct class that the blocking
43
+ // <script> already established — causing a dark→light→dark flash.
44
+ //
45
+ // By calling setUserScheme() here (synchronous, before any component
46
+ // renders), the module-level `currentValue` is correct when useState
47
+ // initializers run, so useUserScheme() returns the right value from
48
+ // the very first render. SchemeProvider's layout effect then sets the
49
+ // correct class immediately, eliminating the flash.
50
+ if (typeof window !== "undefined") {
51
+ try {
52
+ const storedValue = storage.getItem("vxrn-scheme");
53
+ const stored = typeof storedValue === "string" || storedValue === null ? storedValue : null;
54
+ if (stored === "light" || stored === "dark" || stored === "system") {
55
+ setUserScheme(stored);
56
+ } else {
57
+ // No persisted preference — default behavior is "system".
58
+ // Eagerly set "system" so useUserScheme() resolves from matchMedia
59
+ // on the very first render instead of falling back to "light".
60
+ // Without this, React's first render computes activeTheme="light",
61
+ // overriding the t_dark class the blocking script already set,
62
+ // then a useEffect flips it back — causing a light→dark flash.
63
+ setUserScheme("system");
64
+ }
65
+ } catch {
66
+ // storage access might throw in sandboxed contexts
67
+ }
68
+ }
69
+
70
+ i18n.use(initReactI18next).init(createI18nConfig(cfg.i18n)).catch(logger.error);
71
+ i18n.changeLanguage(config.get("I18N_DEFAULT_LANGUAGE", cfg.i18n.defaultLanguage));
72
+
73
+ const sentryDsn = cfg.sentry?.dsn || config.get("SENTRY_DSN");
74
+ if (sentryDsn && !platform.isServer) {
75
+ import("@sentry/react")
76
+ .then((Sentry) => {
77
+ Sentry.init({
78
+ dsn: sentryDsn,
79
+ enabled: !isDev,
80
+ });
81
+ })
82
+ .catch(() => {});
83
+ }
84
+
85
+ const { AppProvider, RootLayout } = cfg;
86
+
87
+ /**
88
+ * Reads the resolved color scheme from `@vxrn/color-scheme` and passes
89
+ * it (along with theme cookies) to the AppProvider / ThemeProvider.
90
+ *
91
+ * Because we eagerly call `setUserScheme()` above (before React renders),
92
+ * `useUserScheme()` returns the correct value from the very first render.
93
+ * No eagerScheme / warmedUp workaround needed.
94
+ */
95
+ function WebRootProvider({ children }: { children: ReactNode }) {
96
+ const { value: scheme } = useUserScheme();
97
+
98
+ return <AppProvider systemTheme={scheme}>{children}</AppProvider>;
99
+ }
100
+ WebRootProvider.displayName = "WebRootProvider";
101
+
102
+ function Layout() {
103
+ // During SSR, read the raw cookie header from the root layout loader
104
+ // so that CookiesProvider (and useCookies/useTheme inside the tree)
105
+ // can resolve the user's persisted theme properties during server
106
+ // rendering. Light/dark is handled separately by SchemeProvider.
107
+ const matches = useMatches();
108
+ const rootLoaderData = matches?.[0]?.loaderData as RootLoaderData | undefined;
109
+ const ssrCookies =
110
+ platform.isServer && rootLoaderData?.cookieHeader
111
+ ? new Cookies(rootLoaderData.cookieHeader)
112
+ : undefined;
113
+
114
+ // Use SSR-detected language (from loader) or fall back to i18next's current language
115
+ const htmlLang =
116
+ rootLoaderData?.detectedLanguage || i18n.language || cfg.i18n.defaultLanguage || "en";
117
+ // Sync i18next to the detected language during SSR so initial render matches
118
+ if (
119
+ platform.isServer &&
120
+ rootLoaderData?.detectedLanguage &&
121
+ i18n.language !== rootLoaderData.detectedLanguage
122
+ ) {
123
+ i18n.changeLanguage(rootLoaderData.detectedLanguage);
124
+ }
125
+
126
+ return (
127
+ <html lang={htmlLang}>
128
+ <head>
129
+ <meta charSet="utf-8" />
130
+ {/* Critical reset CSS with data-precedence so React 19 hoists it
131
+ to the very top of <head> – BEFORE the Tamagui atomic CSS.
132
+ This eliminates FOUC during streaming SSR. */}
133
+ <style precedence="reset" href="__critical_reset">
134
+ {[
135
+ "html{max-width:100vw;color-scheme:light dark}",
136
+ "body{overflow-x:hidden;margin:0;display:flex;flex-direction:column;min-width:100%;min-height:100vh}",
137
+ "#root,#__next{display:flex;flex-direction:column;min-width:100%;min-height:100vh}",
138
+ "*{box-sizing:border-box}",
139
+ "a{color:inherit;text-decoration:none}",
140
+ "h1,h2,h3,h4,h5,h6{margin:0;padding:0}",
141
+ "p,input,textarea,button,ul,ol,li,pre,dialog{all:unset;box-sizing:border-box}",
142
+ "@keyframes skeleton-pulse{0%{opacity:1}50%{opacity:0.4}100%{opacity:1}}",
143
+ ].join("")}
144
+ </style>
145
+ {cfg.head && (
146
+ <>
147
+ <meta httpEquiv="X-UA-Compatible" content="IE=edge" />
148
+ <meta
149
+ name="viewport"
150
+ content="width=device-width, initial-scale=1, maximum-scale=5"
151
+ />
152
+ {cfg.head.favicon && <link rel="icon" href={cfg.head.favicon} />}
153
+ </>
154
+ )}
155
+ {/* tamagui.css is imported via JS in _layout.web.tsx (takeout2 pattern) */}
156
+ </head>
157
+ <body>
158
+ <LoadProgressBar />
159
+ {/* SchemeProvider (@vxrn/color-scheme) injects a blocking inline <script> that reads
160
+ localStorage('vxrn-scheme'), falls back to matchMedia, and sets t_light/t_dark on <html>.
161
+ Edge case: when localStorage throws (e.g. Safari private mode pre-14, or undefined),
162
+ the script handles it gracefully, falls back to matchMedia("(prefers-color-scheme: dark)"),
163
+ and if matchMedia is unavailable (very old browsers) defaults to light. No runtime error,
164
+ no flash. We cannot modify the third-party component — this documents the inherent behavior. */}
165
+ <SchemeProvider>
166
+ <CookiesProvider cookies={ssrCookies}>
167
+ <WebRootProvider>
168
+ <RootLayout ssrSession={rootLoaderData?.session}>
169
+ <Slot />
170
+ </RootLayout>
171
+ </WebRootProvider>
172
+ </CookiesProvider>
173
+ </SchemeProvider>
174
+ </body>
175
+ </html>
176
+ );
177
+ }
178
+ Layout.displayName = "RootLayout";
179
+
180
+ return Layout;
181
+ }
@@ -0,0 +1,138 @@
1
+ import React from "react";
2
+ import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
3
+ import { TanStackDevtools } from "@tanstack/react-devtools";
4
+ import { formDevtoolsPlugin } from "@tanstack/react-form-devtools";
5
+ import { pacerDevtoolsPlugin } from "@tanstack/react-pacer-devtools";
6
+ import { ThemeDevtoolsPanel } from "@multiplatform.one/theme";
7
+ import { storage } from "@multiplatform.one/store";
8
+ import { useEffect, useState } from "react";
9
+ import type { TanstackConfig } from "./CreateApp";
10
+
11
+ const devtoolsSettingsKey = "tanstack_devtools_settings";
12
+
13
+ /**
14
+ * Sync the resolved color scheme into the TanStack Devtools settings
15
+ * stored via the shared storage adapter. The devtools shell reads this on mount and
16
+ * uses it for its own light/dark chrome.
17
+ */
18
+ async function syncDevtoolsTheme(resolvedScheme: "light" | "dark") {
19
+ try {
20
+ const raw = await Promise.resolve(storage.getItem(devtoolsSettingsKey));
21
+ const settings = raw ? JSON.parse(raw) : {};
22
+ if (settings.theme !== resolvedScheme) {
23
+ settings.theme = resolvedScheme;
24
+ await Promise.resolve(storage.setItem(devtoolsSettingsKey, JSON.stringify(settings)));
25
+ }
26
+ } catch {
27
+ // sandboxed / private-mode contexts
28
+ }
29
+ }
30
+
31
+ export interface TanstackDevtoolsProps {
32
+ resolvedScheme: "light" | "dark";
33
+ tanstackConfig: TanstackConfig;
34
+ }
35
+
36
+ /**
37
+ * Lazily-loaded devtools shell. All heavy devtools dependencies
38
+ * (`@tanstack/react-devtools`, `@tanstack/react-query-devtools`,
39
+ * `@tanstack/react-form-devtools`, `@tanstack/react-pacer-devtools`,
40
+ * and `ThemeDevtoolsPanel`) live here so they are code-split away
41
+ * from the production bundle.
42
+ */
43
+ /**
44
+ * Lazily resolves the Frappe devtools plugin. Returns null when no
45
+ * SyncModule is active (e.g. Frappe not configured or not yet initialized).
46
+ * Re-checks periodically until a SyncModule appears.
47
+ */
48
+ function useFrappePlugin() {
49
+ const [plugin, setPlugin] = useState<ReturnType<
50
+ typeof import("@multiplatform.one/frappe/devtools").frappeDevtoolsPlugin
51
+ > | null>(null);
52
+
53
+ useEffect(() => {
54
+ let cancelled = false;
55
+
56
+ async function tryResolve() {
57
+ try {
58
+ const [{ frappeDevtoolsPlugin }, { __getActiveSyncModule }] = await Promise.all([
59
+ import("@multiplatform.one/frappe/devtools"),
60
+ import("@multiplatform.one/frappe"),
61
+ ]);
62
+ const sync = __getActiveSyncModule();
63
+ if (sync && !cancelled) {
64
+ setPlugin(frappeDevtoolsPlugin(sync));
65
+ return true;
66
+ }
67
+ } catch {
68
+ // frappe package not installed or not available — skip
69
+ }
70
+ return false;
71
+ }
72
+
73
+ // Try immediately, then poll a few times in case SyncModule is created later
74
+ void tryResolve().then((found) => {
75
+ if (found || cancelled) return;
76
+ let attempts = 0;
77
+ const interval = setInterval(async () => {
78
+ attempts++;
79
+ const found = await tryResolve();
80
+ if (found || cancelled || attempts >= 10) clearInterval(interval);
81
+ }, 2000);
82
+ return () => clearInterval(interval);
83
+ });
84
+
85
+ return () => {
86
+ cancelled = true;
87
+ };
88
+ }, []);
89
+
90
+ return plugin;
91
+ }
92
+
93
+ export default function TanstackDevtoolsWrapper({
94
+ resolvedScheme,
95
+ tanstackConfig,
96
+ }: TanstackDevtoolsProps) {
97
+ useEffect(() => {
98
+ if (typeof window === "undefined") return;
99
+ void syncDevtoolsTheme(resolvedScheme);
100
+ }, [resolvedScheme]);
101
+
102
+ const frappePlugin = useFrappePlugin();
103
+
104
+ const plugins = [
105
+ {
106
+ id: "react-query",
107
+ name: "React Query",
108
+ render: <ReactQueryDevtools initialIsOpen={false} />,
109
+ },
110
+ formDevtoolsPlugin(),
111
+ pacerDevtoolsPlugin(),
112
+ {
113
+ id: "theme",
114
+ name: "Theme",
115
+ render: (_el: HTMLElement, props: { theme: "dark" | "light" }) => (
116
+ <ThemeDevtoolsPanel
117
+ useUserScheme={tanstackConfig.useUserScheme}
118
+ devtoolsTheme={props.theme}
119
+ tamaguiConfig={tanstackConfig.tamaguiConfig}
120
+ />
121
+ ),
122
+ },
123
+ ...(frappePlugin ? [frappePlugin] : []),
124
+ ];
125
+
126
+ return (
127
+ <TanStackDevtools
128
+ key={resolvedScheme}
129
+ config={{
130
+ defaultOpen: false,
131
+ triggerHidden: true,
132
+ openHotkey: ["CtrlOrMeta", "`"],
133
+ theme: resolvedScheme,
134
+ }}
135
+ plugins={plugins}
136
+ />
137
+ );
138
+ }
@@ -0,0 +1,41 @@
1
+ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
2
+ import { Suspense, lazy, type PropsWithChildren } from "react";
3
+ import { platform } from "@multiplatform.one/platform";
4
+ import type { ProviderComponent, TanstackConfig } from "./CreateApp";
5
+
6
+ const LazyDevtools = lazy(() => import("./TanstackDevtools"));
7
+
8
+ export function createTanstackProvider(tanstackConfig: TanstackConfig): ProviderComponent {
9
+ const queryClient = new QueryClient({
10
+ defaultOptions: {
11
+ queries: {
12
+ staleTime: 30 * 1000,
13
+ retryDelay: (attempt) => Math.min(1000 * 2 ** attempt, 10_000),
14
+ },
15
+ },
16
+ ...tanstackConfig.queryClientOptions,
17
+ });
18
+
19
+ function TanstackProvider({ children }: PropsWithChildren) {
20
+ // TODO(native): TanStack Devtools does not yet support React Native. When it does,
21
+ // revisit this guard to also handle native (e.g. !platform.isNative) so devtools
22
+ // are enabled on native when supported.
23
+ const showDevtools = tanstackConfig.debug && !platform.isStorybook;
24
+
25
+ const scheme = tanstackConfig.useUserScheme?.();
26
+ const resolvedScheme = scheme?.value ?? "light";
27
+
28
+ return (
29
+ <QueryClientProvider client={queryClient}>
30
+ {showDevtools && (
31
+ <Suspense>
32
+ <LazyDevtools resolvedScheme={resolvedScheme} tanstackConfig={tanstackConfig} />
33
+ </Suspense>
34
+ )}
35
+ {children}
36
+ </QueryClientProvider>
37
+ );
38
+ }
39
+ TanstackProvider.displayName = "TanstackProvider";
40
+ return TanstackProvider;
41
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * ## Platform File Extension Conventions
3
+ *
4
+ * The framework uses platform-specific file extensions for automatic
5
+ * bundler resolution. When multiple variants exist for a module, the
6
+ * bundler picks the most specific match for the current target:
7
+ *
8
+ * | Extension | Target |
9
+ * | ---------------- | --------------------------------------- |
10
+ * | `.web.tsx` | Web (browser + SSR) |
11
+ * | `.native.tsx` | React Native (iOS + Android) |
12
+ * | `.ios.tsx` | iOS only |
13
+ * | `.android.tsx` | Android only |
14
+ * | `.tauri.tsx` | Tauri desktop (macOS, Windows, Linux) |
15
+ * | `.storybook-expo.ts` | Storybook running in Expo |
16
+ * | (no suffix) | Universal fallback / shared code |
17
+ *
18
+ * Resolution priority (most → least specific):
19
+ * `.ios.tsx` > `.native.tsx` > `.tsx` (on iOS)
20
+ * `.web.tsx` > `.tsx` (on web)
21
+ * `.tauri.tsx` > `.tsx` (on Tauri)
22
+ *
23
+ * When creating platform-specific variants, always keep a universal
24
+ * fallback file (e.g. `index.ts`) that exports the default API.
25
+ */
26
+ export * from "./CreateApp";
27
+ export * from "./CreateRootLayout";
@@ -0,0 +1,7 @@
1
+ import type { ComponentType, PropsWithChildren } from "react";
2
+
3
+ // Type stub for the platform-split loadWalletProvider (loadWalletProvider.native.ts /
4
+ // loadWalletProvider.web.ts). The bundler resolves the concrete file per platform;
5
+ // this lets `import LazyWalletProvider from "./loadWalletProvider"` typecheck.
6
+ declare const WalletProvider: ComponentType<PropsWithChildren<Record<string, unknown>>>;
7
+ export default WalletProvider;
@@ -0,0 +1,14 @@
1
+ import { lazy } from "react";
2
+
3
+ // Native loads the web3 WalletProvider through the `@multiplatform.one/web3/native`
4
+ // subpath, whose export conditions ALL resolve to the bridge-only native build
5
+ // (src .native variants / dist/esm/index.native.js). The bare `.` package must keep
6
+ // its `import` condition pointing at the WEB dist (.mjs) for the web bundle — but a
7
+ // dynamic import of the bare specifier on native resolves that same web dist and
8
+ // drags connectkit/@reown/@walletconnect (+ their Node-builtin deps: pino,
9
+ // thread-stream, qrcode/pngjs, …) into the Hermes bundle as dead weight. Routing
10
+ // native through `/native` keeps that entire stack out of native, where the wallet
11
+ // connect/modal UI lives in the WebView via the bridge instead.
12
+ export default lazy(() =>
13
+ import("@multiplatform.one/web3/native").then((mod) => ({ default: mod.WalletProvider })),
14
+ );
@@ -0,0 +1,10 @@
1
+ import { Fragment, createElement, type ComponentType, type PropsWithChildren } from "react";
2
+
3
+ // On web, CreateApp's Web3Provider returns children (the WagmiProvider is mounted at
4
+ // screen scope in features/wallet/Web3ScreenProvider.web, below the navigator, to
5
+ // avoid the SSR/"Another navigator is already registered" problem). So this provider
6
+ // is NEVER rendered on web — it exists only so the shared CreateApp can import a single
7
+ // symbol without pulling the web3 package into the core bootstrap path on web.
8
+ const WalletProvider: ComponentType<PropsWithChildren<Record<string, unknown>>> = ({ children }) =>
9
+ createElement(Fragment, null, children);
10
+ export default WalletProvider;
@@ -0,0 +1,60 @@
1
+ import type { ComponentType, PropsWithChildren } from "react";
2
+ import type { I18nConfigOptions } from "@multiplatform.one/i18n";
3
+ import type { AppProviderProps } from "./CreateApp";
4
+
5
+ export interface HeadConfig {
6
+ /** Path to favicon (e.g. "/favicon.svg"). */
7
+ favicon?: string;
8
+ }
9
+
10
+ export interface SentryConfig {
11
+ /** Sentry DSN. When provided, Sentry is initialized on the web client. */
12
+ dsn?: string;
13
+ }
14
+
15
+ /** Shape of the root layout loader data used by createRootLayout. */
16
+ export interface RootLoaderData {
17
+ cookieHeader?: string;
18
+ /** Language detected from request (cookie or Accept-Language header). */
19
+ detectedLanguage?: string;
20
+ /** Server-side session from the auth loader (used for SSR hydration). */
21
+ session?: unknown;
22
+ [key: string]: unknown;
23
+ }
24
+
25
+ /**
26
+ * Props accepted by the `RootLayout` component.
27
+ *
28
+ * `ssrSession` is provided only on web during SSR so auth-related UI
29
+ * (e.g. user name, logout button) can render on the first paint without
30
+ * waiting for a client-side session fetch.
31
+ */
32
+ export interface RootLayoutProps extends PropsWithChildren {
33
+ /** Server-side session data for SSR hydration (web only). */
34
+ ssrSession?: unknown;
35
+ }
36
+
37
+ /**
38
+ * Configuration for `createRootLayout()`.
39
+ *
40
+ * The factory initializes i18n, detects color scheme, wraps with the
41
+ * appropriate platform providers, and renders the RootLayout + Slot.
42
+ */
43
+ export interface CreateRootLayoutConfig {
44
+ /** The AppProvider component returned from `createApp()`. */
45
+ AppProvider: ComponentType<AppProviderProps>;
46
+ /** Root layout shell that wraps route content (navigation chrome, header, etc.). */
47
+ RootLayout: ComponentType<RootLayoutProps>;
48
+ /** i18n configuration. i18next is initialized as a side-effect when the factory is called. */
49
+ i18n: I18nConfigOptions;
50
+ /** `<Head>` meta tags for web. Ignored on native. */
51
+ head?: HeadConfig;
52
+ /** Sentry error tracking. Initialized on the web client only. Ignored on native. */
53
+ sentry?: SentryConfig;
54
+ /**
55
+ * Native font map returned by `importFonts()` from `@package/fonts`.
56
+ * Passed to `useFonts()` so custom fonts load before the first render.
57
+ * Ignored on web (fonts are loaded via CSS side-effect imports).
58
+ */
59
+ fonts?: Record<string, number>;
60
+ }
@@ -0,0 +1,13 @@
1
+ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
2
+ import type { PropsWithChildren } from "react";
3
+ import type { ProviderComponent, TanstackConfig } from "./CreateApp";
4
+
5
+ export function createTanstackProvider(tanstackConfig: TanstackConfig): ProviderComponent {
6
+ const queryClient = new QueryClient(tanstackConfig.queryClientOptions);
7
+
8
+ function TanstackProvider({ children }: PropsWithChildren) {
9
+ return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>;
10
+ }
11
+ TanstackProvider.displayName = "TanstackProvider";
12
+ return TanstackProvider;
13
+ }
package/src/healthz.ts ADDED
@@ -0,0 +1,4 @@
1
+ /** Health check handler compatible with One framework +api.ts convention. */
2
+ export async function GET(_request: Request) {
3
+ return new Response(JSON.stringify({ status: "ok" }));
4
+ }
package/src/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export * from "./app/index";
2
+ export * from "./layouts/index";