feat: turbostarter boilerplate
Production-ready Next.js boilerplate with: - Runtime env validation (fail-fast on missing vars) - Feature-gated config (S3, Stripe, email, OAuth) - Docker + Coolify deployment pipeline - PostgreSQL + pgvector, MinIO S3, Better Auth - TypeScript strict mode (no ignoreBuildErrors) - i18n (en/es), AI modules, billing, monitoring Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
4
packages/analytics/mobile/eslint.config.js
Normal file
4
packages/analytics/mobile/eslint.config.js
Normal file
@@ -0,0 +1,4 @@
|
||||
import baseConfig from "@turbostarter/eslint-config/base";
|
||||
import reactConfig from "@turbostarter/eslint-config/react";
|
||||
|
||||
export default [...baseConfig, ...reactConfig];
|
||||
34
packages/analytics/mobile/package.json
Normal file
34
packages/analytics/mobile/package.json
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"name": "@turbostarter/analytics-mobile",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./env": "./src/env.ts"
|
||||
},
|
||||
"scripts": {
|
||||
"clean": "git clean -xdf .cache .turbo dist node_modules",
|
||||
"format": "prettier --check . --ignore-path ../../../.gitignore",
|
||||
"lint": "eslint",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@turbostarter/eslint-config": "workspace:*",
|
||||
"@turbostarter/prettier-config": "workspace:*",
|
||||
"@turbostarter/tsconfig": "workspace:*",
|
||||
"eslint": "catalog:",
|
||||
"prettier": "catalog:",
|
||||
"typescript": "catalog:"
|
||||
},
|
||||
"prettier": "@turbostarter/prettier-config",
|
||||
"dependencies": {
|
||||
"@turbostarter/analytics": "workspace:*",
|
||||
"@turbostarter/shared": "workspace:*",
|
||||
"envin": "catalog:",
|
||||
"mixpanel-react-native": "3.1.2",
|
||||
"posthog-react-native": "4.14.3",
|
||||
"react-native": "catalog:",
|
||||
"zod": "catalog:"
|
||||
}
|
||||
}
|
||||
1
packages/analytics/mobile/src/env.ts
Normal file
1
packages/analytics/mobile/src/env.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { env, preset } from "./providers";
|
||||
1
packages/analytics/mobile/src/hooks/index.ts
Normal file
1
packages/analytics/mobile/src/hooks/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./use-tracking-permissions";
|
||||
@@ -0,0 +1,32 @@
|
||||
import { requestTrackingPermissionsAsync } from "expo-tracking-transparency";
|
||||
import { useEffect, useState, useCallback } from "react";
|
||||
import { AppState } from "react-native";
|
||||
|
||||
export const useTrackingPermissions = () => {
|
||||
const [granted, setGranted] = useState(false);
|
||||
|
||||
const checkPermission = useCallback(async () => {
|
||||
const { granted: isGranted } = await requestTrackingPermissionsAsync();
|
||||
setGranted(isGranted);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
void checkPermission();
|
||||
}, [checkPermission]);
|
||||
|
||||
useEffect(() => {
|
||||
const subscription = AppState.addEventListener("change", (status) => {
|
||||
if (status !== "active") {
|
||||
return;
|
||||
}
|
||||
|
||||
void checkPermission();
|
||||
});
|
||||
|
||||
return () => {
|
||||
subscription.remove();
|
||||
};
|
||||
}, [checkPermission]);
|
||||
|
||||
return granted;
|
||||
};
|
||||
3
packages/analytics/mobile/src/index.ts
Normal file
3
packages/analytics/mobile/src/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { Provider, track, identify, reset } from "./providers";
|
||||
|
||||
export * from "./hooks";
|
||||
@@ -0,0 +1,16 @@
|
||||
import { defineEnv } from "envin";
|
||||
|
||||
import { envConfig } from "@turbostarter/shared/constants";
|
||||
|
||||
import type { Preset } from "envin/types";
|
||||
|
||||
export const preset = {
|
||||
id: "google-analytics",
|
||||
clientPrefix: "EXPO_PUBLIC_",
|
||||
client: {},
|
||||
} as const satisfies Preset;
|
||||
|
||||
export const env = defineEnv({
|
||||
...envConfig,
|
||||
...preset,
|
||||
});
|
||||
@@ -0,0 +1,69 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-nocheck
|
||||
import analytics from "@react-native-firebase/analytics";
|
||||
import { useGlobalSearchParams, usePathname } from "expo-router";
|
||||
import { useEffect } from "react";
|
||||
|
||||
import { useTrackingPermissions } from "../../hooks";
|
||||
|
||||
import type { AnalyticsProviderClientStrategy } from "@turbostarter/analytics";
|
||||
|
||||
const setup = async () => {
|
||||
await analytics().setAnalyticsCollectionEnabled(true);
|
||||
await analytics().setConsent({
|
||||
analytics_storage: true,
|
||||
ad_storage: true,
|
||||
ad_user_data: true,
|
||||
ad_personalization: true,
|
||||
});
|
||||
};
|
||||
|
||||
const useSetup = () => {
|
||||
const granted = useTrackingPermissions();
|
||||
const pathname = usePathname();
|
||||
const params = useGlobalSearchParams();
|
||||
|
||||
useEffect(() => {
|
||||
if (!granted) {
|
||||
return;
|
||||
}
|
||||
|
||||
void setup();
|
||||
}, [granted]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!granted) {
|
||||
return;
|
||||
}
|
||||
|
||||
void analytics().logScreenView({
|
||||
screen_name: pathname,
|
||||
screen_class: pathname,
|
||||
params,
|
||||
});
|
||||
}, [pathname, params, granted]);
|
||||
};
|
||||
|
||||
export const { Provider, track, identify, reset } = {
|
||||
Provider: ({ children }) => {
|
||||
useSetup();
|
||||
|
||||
return children;
|
||||
},
|
||||
track: (name, params) => {
|
||||
void analytics().logEvent(name, params);
|
||||
},
|
||||
identify: (userId, traits) => {
|
||||
void analytics().setUserId(userId);
|
||||
|
||||
if (traits) {
|
||||
void analytics().setUserProperties(traits);
|
||||
}
|
||||
},
|
||||
reset: () => {
|
||||
void analytics().setUserId(null);
|
||||
void analytics().setUserProperties({});
|
||||
},
|
||||
} satisfies AnalyticsProviderClientStrategy;
|
||||
2
packages/analytics/mobile/src/providers/index.ts
Normal file
2
packages/analytics/mobile/src/providers/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from "./mixpanel";
|
||||
export * from "./mixpanel/env";
|
||||
24
packages/analytics/mobile/src/providers/mixpanel/env.ts
Normal file
24
packages/analytics/mobile/src/providers/mixpanel/env.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
/* eslint-disable turbo/no-undeclared-env-vars */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
import { defineEnv } from "envin";
|
||||
import * as z from "zod";
|
||||
|
||||
import { envConfig } from "@turbostarter/shared/constants";
|
||||
|
||||
import type { Preset } from "envin/types";
|
||||
|
||||
export const preset = {
|
||||
id: "mixpanel",
|
||||
clientPrefix: "EXPO_PUBLIC_",
|
||||
client: {
|
||||
EXPO_PUBLIC_MIXPANEL_TOKEN: z.string(),
|
||||
},
|
||||
} as const satisfies Preset;
|
||||
|
||||
export const env = defineEnv({
|
||||
...envConfig,
|
||||
...preset,
|
||||
env: {
|
||||
EXPO_PUBLIC_MIXPANEL_TOKEN: process.env.EXPO_PUBLIC_MIXPANEL_TOKEN,
|
||||
},
|
||||
});
|
||||
47
packages/analytics/mobile/src/providers/mixpanel/index.tsx
Normal file
47
packages/analytics/mobile/src/providers/mixpanel/index.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import { Mixpanel } from "mixpanel-react-native";
|
||||
import { useEffect } from "react";
|
||||
|
||||
import { useTrackingPermissions } from "../../hooks";
|
||||
|
||||
import { env } from "./env";
|
||||
|
||||
import type { AnalyticsProviderClientStrategy } from "@turbostarter/analytics";
|
||||
|
||||
const optOutTracking = true;
|
||||
const trackAutomaticEvents = false;
|
||||
const mixpanel = new Mixpanel(
|
||||
env.EXPO_PUBLIC_MIXPANEL_TOKEN,
|
||||
trackAutomaticEvents,
|
||||
optOutTracking,
|
||||
);
|
||||
|
||||
void mixpanel.init();
|
||||
|
||||
export const { Provider, track, identify, reset } = {
|
||||
Provider: ({ children }) => {
|
||||
const granted = useTrackingPermissions();
|
||||
|
||||
useEffect(() => {
|
||||
void (async () => {
|
||||
const optedOut = await mixpanel.hasOptedOutTracking();
|
||||
if (granted && optedOut) {
|
||||
void mixpanel.optInTracking();
|
||||
}
|
||||
})();
|
||||
}, [granted]);
|
||||
|
||||
return <>{children}</>;
|
||||
},
|
||||
track: (name, params) => {
|
||||
mixpanel.track(name, params);
|
||||
},
|
||||
identify: (userId, traits) => {
|
||||
void mixpanel.identify(userId);
|
||||
if (traits) {
|
||||
void mixpanel.getPeople().set(traits);
|
||||
}
|
||||
},
|
||||
reset: () => {
|
||||
mixpanel.reset();
|
||||
},
|
||||
} satisfies AnalyticsProviderClientStrategy;
|
||||
29
packages/analytics/mobile/src/providers/posthog/env.ts
Normal file
29
packages/analytics/mobile/src/providers/posthog/env.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
/* eslint-disable turbo/no-undeclared-env-vars */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
import { defineEnv } from "envin";
|
||||
import * as z from "zod";
|
||||
|
||||
import { envConfig } from "@turbostarter/shared/constants";
|
||||
|
||||
import type { Preset } from "envin/types";
|
||||
|
||||
export const preset = {
|
||||
id: "posthog",
|
||||
clientPrefix: "EXPO_PUBLIC_",
|
||||
client: {
|
||||
EXPO_PUBLIC_POSTHOG_KEY: z.string(),
|
||||
EXPO_PUBLIC_POSTHOG_HOST: z
|
||||
.string()
|
||||
.optional()
|
||||
.default("https://us.i.posthog.com"),
|
||||
},
|
||||
} as const satisfies Preset;
|
||||
|
||||
export const env = defineEnv({
|
||||
...envConfig,
|
||||
...preset,
|
||||
env: {
|
||||
EXPO_PUBLIC_POSTHOG_KEY: process.env.EXPO_PUBLIC_POSTHOG_KEY,
|
||||
EXPO_PUBLIC_POSTHOG_HOST: process.env.EXPO_PUBLIC_POSTHOG_HOST,
|
||||
},
|
||||
});
|
||||
73
packages/analytics/mobile/src/providers/posthog/index.tsx
Normal file
73
packages/analytics/mobile/src/providers/posthog/index.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
import PostHog, { PostHogProvider } from "posthog-react-native";
|
||||
import { useEffect } from "react";
|
||||
|
||||
import { useTrackingPermissions } from "../../hooks";
|
||||
|
||||
import { env } from "./env";
|
||||
|
||||
import type { AnalyticsProviderClientStrategy } from "@turbostarter/analytics";
|
||||
|
||||
let client: PostHog | null = null;
|
||||
|
||||
const getClient = () => {
|
||||
if (client) {
|
||||
return client;
|
||||
}
|
||||
|
||||
client = new PostHog(env.EXPO_PUBLIC_POSTHOG_KEY, {
|
||||
host: env.EXPO_PUBLIC_POSTHOG_HOST,
|
||||
defaultOptIn: false,
|
||||
});
|
||||
|
||||
return client;
|
||||
};
|
||||
|
||||
const Wrapper = ({ children }: { children: React.ReactNode }) => {
|
||||
const client = getClient();
|
||||
|
||||
return (
|
||||
<PostHogProvider client={client} autocapture>
|
||||
{children}
|
||||
</PostHogProvider>
|
||||
);
|
||||
};
|
||||
|
||||
const Setup = () => {
|
||||
const client = getClient();
|
||||
const granted = useTrackingPermissions();
|
||||
|
||||
useEffect(() => {
|
||||
if (granted) {
|
||||
void client.optIn();
|
||||
} else {
|
||||
void client.optOut();
|
||||
}
|
||||
}, [granted, client]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const ProviderComponent = ({ children }: { children: React.ReactNode }) => {
|
||||
return (
|
||||
<Wrapper>
|
||||
<Setup />
|
||||
{children}
|
||||
</Wrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export const { Provider, track, identify, reset } = {
|
||||
Provider: ProviderComponent,
|
||||
track: (name, params) => {
|
||||
const client = getClient();
|
||||
client.capture(name, params);
|
||||
},
|
||||
identify: (userId, traits) => {
|
||||
const client = getClient();
|
||||
client.identify(userId, traits);
|
||||
},
|
||||
reset: () => {
|
||||
const client = getClient();
|
||||
client.reset();
|
||||
},
|
||||
} satisfies AnalyticsProviderClientStrategy;
|
||||
9
packages/analytics/mobile/tsconfig.json
Normal file
9
packages/analytics/mobile/tsconfig.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "@turbostarter/tsconfig/internal.json",
|
||||
"compilerOptions": {
|
||||
"lib": ["dom"],
|
||||
"jsx": "preserve"
|
||||
},
|
||||
"include": ["*.ts", "src/**/*"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
Reference in New Issue
Block a user