feat: whyrating - initial project from turbostarter boilerplate

This commit is contained in:
Alejandro Gutiérrez
2026-02-04 01:54:52 +01:00
commit 5cdc07cd39
1618 changed files with 338230 additions and 0 deletions

View File

@@ -0,0 +1,21 @@
import { hc } from "hono/client";
import { config } from "@turbostarter/i18n";
import { authClient } from "~/lib/auth";
import { useI18nConfig } from "~/lib/providers/i18n";
import { getBaseUrl } from "./utils";
import type { AppRouter } from "@turbostarter/api";
export const { api } = hc<AppRouter>(getBaseUrl(), {
headers: () => ({
cookie: `${config.cookie}=${useI18nConfig.getState().config.locale};${authClient.getCookie()}`,
"x-client-platform": "mobile",
}),
init: {
/* https://github.com/better-auth/better-auth/issues/2970 */
credentials: "omit",
},
});

View File

@@ -0,0 +1,23 @@
import env from "env.config";
import Constants from "expo-constants";
import { logger } from "@turbostarter/shared/logger";
export const getBaseUrl = () => {
/**
* Gets the IP address of your host-machine. If it cannot automatically find it,
* you'll have to manually set it. NOTE: Port 3000 should work for most but confirm
* you don't have anything else running on it, or you'd have to change it.
*
* **NOTE**: This is only for development. In production, you'll want to set the
* baseUrl to your production API URL.
*/
const debuggerHost = Constants.expoConfig?.hostUri;
const localhost = debuggerHost?.split(":")[0];
if (!localhost) {
logger.warn("Failed to get localhost. Pointing to production server...");
return env.EXPO_PUBLIC_SITE_URL;
}
return `http://${localhost}:3000`;
};

View File

@@ -0,0 +1,26 @@
import * as SecureStore from "expo-secure-store";
import { createClient } from "@turbostarter/auth/client/mobile";
import { config } from "@turbostarter/i18n";
import { getBaseUrl } from "~/lib/api/utils";
import { useI18nConfig } from "~/lib/providers/i18n";
export const authClient = createClient({
baseURL: getBaseUrl(),
disableDefaultFetchPlugins: true,
mobile: {
storage: SecureStore,
cookiePrefix: "turbostarter",
},
lastLoginMethod: {
storage: SecureStore,
},
fetchOptions: {
headers: {
Cookie: `${config.cookie}=${useI18nConfig.getState().config.locale}`,
"x-client-platform": "mobile",
},
throw: true,
},
});

View File

@@ -0,0 +1,28 @@
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-unsafe-call */
import structuredClone from "@ungap/structured-clone";
import { Platform } from "react-native";
if (Platform.OS !== "web") {
const setupPolyfills = async () => {
const { polyfillGlobal } = await import(
// @ts-expect-error - polyfillGlobal is not typed
"react-native/Libraries/Utilities/PolyfillFunctions"
);
const { TextEncoderStream, TextDecoderStream } = await import(
"@stardazed/streams-text-encoding"
);
if (!("structuredClone" in global)) {
polyfillGlobal("structuredClone", () => structuredClone);
}
polyfillGlobal("TextEncoderStream", () => TextEncoderStream);
polyfillGlobal("TextDecoderStream", () => TextDecoderStream);
};
void setupPolyfills();
}
export {};

View File

@@ -0,0 +1,28 @@
import { useEffect } from "react";
import { identify, Provider, reset } from "@turbostarter/analytics-mobile";
import { authClient } from "~/lib/auth";
export const AnalyticsProvider = ({
children,
}: {
children: React.ReactNode;
}) => {
const session = authClient.useSession();
useEffect(() => {
if (session.isPending) {
return;
}
if (session.data?.user) {
const { id, email, name } = session.data.user;
identify(id, { email, name });
} else {
reset();
}
}, [session]);
return <Provider>{children}</Provider>;
};

View File

@@ -0,0 +1,43 @@
import AsyncStorage from "@react-native-async-storage/async-storage";
import { getLocales } from "expo-localization";
import { memo } from "react";
import { create } from "zustand";
import { createJSONStorage, persist } from "zustand/middleware";
import { config, I18nProvider as I18nClientProvider } from "@turbostarter/i18n";
import { appConfig } from "~/config/app";
export const useI18nConfig = create<{
config: {
locale?: string;
};
setConfig: (config: { locale?: string }) => void;
}>()(
persist(
(set) => ({
config: { locale: getLocales()[0]?.languageCode ?? config.defaultLocale },
setConfig: (config) => set({ config }),
}),
{
name: "i18n-config",
storage: createJSONStorage(() => AsyncStorage),
},
),
);
interface I18nProviderProps {
readonly children: React.ReactNode;
}
export const I18nProvider = memo<I18nProviderProps>(({ children }) => {
const config = useI18nConfig((state) => state.config);
return (
<I18nClientProvider locale={config.locale} defaultLocale={appConfig.locale}>
{children}
</I18nClientProvider>
);
});
I18nProvider.displayName = "I18nProvider";

View File

@@ -0,0 +1,25 @@
import { useEffect } from "react";
import { initialize, identify } from "@turbostarter/monitoring-mobile";
import { authClient } from "~/lib/auth";
initialize();
export const MonitoringProvider = ({
children,
}: {
children: React.ReactNode;
}) => {
const session = authClient.useSession();
useEffect(() => {
if (session.isPending) {
return;
}
identify(session.data?.user ?? null);
}, [session]);
return children;
};

View File

@@ -0,0 +1,52 @@
import { BottomSheetModalProvider } from "@gorhom/bottom-sheet";
import { PortalHost } from "@rn-primitives/portal";
import dayjs from "dayjs";
import duration from "dayjs/plugin/duration";
import relativeTime from "dayjs/plugin/relativeTime";
import { memo } from "react";
import { GestureHandlerRootView } from "react-native-gesture-handler";
import { KeyboardProvider } from "react-native-keyboard-controller";
import { SafeAreaProvider } from "react-native-safe-area-context";
import { I18nProvider } from "~/lib/providers/i18n";
import { ThemeProvider } from "~/lib/providers/theme";
import { QueryClientProvider } from "~/lib/query";
import { Verification } from "~/modules/auth/verification";
import { AnalyticsProvider } from "./analytics";
import { MonitoringProvider } from "./monitoring";
dayjs.extend(duration);
dayjs.extend(relativeTime);
interface ProvidersProps {
readonly children: React.ReactNode;
}
export const Providers = memo<ProvidersProps>(({ children }) => {
return (
<GestureHandlerRootView>
<QueryClientProvider>
<I18nProvider>
<SafeAreaProvider>
<ThemeProvider>
<KeyboardProvider>
<BottomSheetModalProvider>
<MonitoringProvider>
<AnalyticsProvider>
{children}
<Verification />
<PortalHost />
</AnalyticsProvider>
</MonitoringProvider>
</BottomSheetModalProvider>
</KeyboardProvider>
</ThemeProvider>
</SafeAreaProvider>
</I18nProvider>
</QueryClientProvider>
</GestureHandlerRootView>
);
});
Providers.displayName = "Providers";

View File

@@ -0,0 +1,44 @@
import {
DarkTheme,
DefaultTheme,
ThemeProvider as NavigationThemeProvider,
} from "@react-navigation/native";
import * as NavigationBar from "expo-navigation-bar";
import { memo } from "react";
import { StatusBar, View } from "react-native";
import { ThemeMode } from "@turbostarter/ui";
import { useTheme } from "~/modules/common/hooks/use-theme";
import { isAndroid } from "~/utils/device";
interface ThemeProviderProps {
readonly children: React.ReactNode;
}
export const ThemeProvider = memo<ThemeProviderProps>(({ children }) => {
const { resolvedTheme } = useTheme();
if (isAndroid) {
void NavigationBar.setButtonStyleAsync(
resolvedTheme === ThemeMode.DARK ? ThemeMode.LIGHT : ThemeMode.DARK,
);
}
return (
<NavigationThemeProvider
value={resolvedTheme === ThemeMode.DARK ? DarkTheme : DefaultTheme}
>
<View className="bg-background flex-1">{children}</View>
<StatusBar
barStyle={
resolvedTheme === ThemeMode.DARK ? "light-content" : "dark-content"
}
translucent
backgroundColor="transparent"
/>
</NavigationThemeProvider>
);
});
ThemeProvider.displayName = "ThemeProvider";

View File

@@ -0,0 +1,49 @@
import { useReactQueryDevTools } from "@dev-plugins/react-query";
import {
QueryClient,
QueryClientProvider as TanstackQueryClientProvider,
} from "@tanstack/react-query";
import { onlineManager } from "@tanstack/react-query";
import * as Network from "expo-network";
import { useState } from "react";
import { Alert } from "react-native";
import { logger } from "@turbostarter/shared/logger";
import { useRefetchOnAppFocus } from "~/modules/common/hooks/use-refetch-on-app-focus";
onlineManager.setEventListener((setOnline) => {
const eventSubscription = Network.addNetworkStateListener((state) => {
setOnline(!!state.isConnected);
});
return () => eventSubscription.remove();
});
export function QueryClientProvider(props: { children: React.ReactNode }) {
const [queryClient] = useState(
() =>
new QueryClient({
defaultOptions: {
mutations: {
onError: (error: Error | { error: Error }) => {
if ("error" in error) {
error = error.error;
}
logger.error(error);
Alert.alert(error.message);
},
},
},
}),
);
useRefetchOnAppFocus();
useReactQueryDevTools(queryClient);
return (
<TanstackQueryClientProvider client={queryClient}>
{props.children}
</TanstackQueryClientProvider>
);
}