feat: whyrating - initial project from turbostarter boilerplate
This commit is contained in:
21
apps/mobile/src/lib/api/index.tsx
Normal file
21
apps/mobile/src/lib/api/index.tsx
Normal 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",
|
||||
},
|
||||
});
|
||||
23
apps/mobile/src/lib/api/utils.ts
Normal file
23
apps/mobile/src/lib/api/utils.ts
Normal 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`;
|
||||
};
|
||||
26
apps/mobile/src/lib/auth/index.ts
Normal file
26
apps/mobile/src/lib/auth/index.ts
Normal 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,
|
||||
},
|
||||
});
|
||||
28
apps/mobile/src/lib/polyfills.ts
Normal file
28
apps/mobile/src/lib/polyfills.ts
Normal 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 {};
|
||||
28
apps/mobile/src/lib/providers/analytics.tsx
Normal file
28
apps/mobile/src/lib/providers/analytics.tsx
Normal 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>;
|
||||
};
|
||||
43
apps/mobile/src/lib/providers/i18n.tsx
Normal file
43
apps/mobile/src/lib/providers/i18n.tsx
Normal 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";
|
||||
25
apps/mobile/src/lib/providers/monitoring.tsx
Normal file
25
apps/mobile/src/lib/providers/monitoring.tsx
Normal 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;
|
||||
};
|
||||
52
apps/mobile/src/lib/providers/providers.tsx
Normal file
52
apps/mobile/src/lib/providers/providers.tsx
Normal 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";
|
||||
44
apps/mobile/src/lib/providers/theme.tsx
Normal file
44
apps/mobile/src/lib/providers/theme.tsx
Normal 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";
|
||||
49
apps/mobile/src/lib/query.tsx
Normal file
49
apps/mobile/src/lib/query.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user