codingpixel-expo-app 0.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.
Files changed (83) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +112 -0
  3. package/bin/cli.js +9 -0
  4. package/dist/babel.js +198 -0
  5. package/dist/bootstrap.js +54 -0
  6. package/dist/fonts.js +98 -0
  7. package/dist/index.js +120 -0
  8. package/dist/install.js +243 -0
  9. package/dist/overlay.js +141 -0
  10. package/dist/patch.js +212 -0
  11. package/dist/prompts.js +115 -0
  12. package/dist/scaffold.js +43 -0
  13. package/dist/sdkNotes.js +19 -0
  14. package/dist/util.js +48 -0
  15. package/package.json +56 -0
  16. package/templates/base/assets/fonts/.gitkeep +0 -0
  17. package/templates/base/assets/images/.gitkeep +0 -0
  18. package/templates/base/assets/index.ts +8 -0
  19. package/templates/base/src/app/_layout.tsx +39 -0
  20. package/templates/base/src/app/index.tsx +13 -0
  21. package/templates/base/src/app/routes.tsx +9 -0
  22. package/templates/base/src/core/hooks/.gitkeep +0 -0
  23. package/templates/base/src/core/redux/hooks.ts +6 -0
  24. package/templates/base/src/core/redux/mmkvStorage.ts +18 -0
  25. package/templates/base/src/core/redux/reducers.ts +20 -0
  26. package/templates/base/src/core/redux/slices/userSlice.ts +31 -0
  27. package/templates/base/src/core/redux/store.ts +28 -0
  28. package/templates/base/src/core/services/.gitkeep +0 -0
  29. package/templates/base/src/core/tanstack/index.tsx +93 -0
  30. package/templates/base/src/core/tanstack/tanstack-keys.ts +42 -0
  31. package/templates/base/src/core/utils/config.ts +50 -0
  32. package/templates/base/src/core/utils/constants.ts +11 -0
  33. package/templates/base/src/core/utils/endpoints.ts +29 -0
  34. package/templates/base/src/core/utils/types.ts +47 -0
  35. package/templates/base/src/core/utils/validation.ts +10 -0
  36. package/templates/base/src/features/.gitkeep +0 -0
  37. package/templates/base/src/ui/appComponents/AppRefreshControl/index.tsx +39 -0
  38. package/templates/base/src/ui/appComponents/appButton/index.tsx +122 -0
  39. package/templates/base/src/ui/appComponents/appColumnView/index.tsx +320 -0
  40. package/templates/base/src/ui/appComponents/appFlashList/index.tsx +191 -0
  41. package/templates/base/src/ui/appComponents/appFlatList/index.tsx +172 -0
  42. package/templates/base/src/ui/appComponents/appIcon/index.tsx +105 -0
  43. package/templates/base/src/ui/appComponents/appInput/index.tsx +226 -0
  44. package/templates/base/src/ui/appComponents/appKeyboardAvoidingView/index.tsx +168 -0
  45. package/templates/base/src/ui/appComponents/appKeyboardAwareScrollView/index.tsx +188 -0
  46. package/templates/base/src/ui/appComponents/appLogger/index.tsx +22 -0
  47. package/templates/base/src/ui/appComponents/appPressable/index.tsx +220 -0
  48. package/templates/base/src/ui/appComponents/appRowView/index.tsx +320 -0
  49. package/templates/base/src/ui/appComponents/appSafeAreaInsets/index.tsx +24 -0
  50. package/templates/base/src/ui/appComponents/appScrollView/index.tsx +166 -0
  51. package/templates/base/src/ui/appComponents/appSkeleton/index.tsx +22 -0
  52. package/templates/base/src/ui/appComponents/appStatusBar/index.tsx +8 -0
  53. package/templates/base/src/ui/appComponents/appTabHeader/index.tsx +111 -0
  54. package/templates/base/src/ui/appComponents/appText/index.tsx +258 -0
  55. package/templates/base/src/ui/appComponents/appTextWrapper/index.tsx +84 -0
  56. package/templates/base/src/ui/appComponents/appWrapper/index.tsx +56 -0
  57. package/templates/base/src/ui/appComponents/customActivityIndicator/index.tsx +34 -0
  58. package/templates/base/src/ui/appComponents/customGetPermissionModal/index.tsx +62 -0
  59. package/templates/base/src/ui/appComponents/customModal/index.tsx +26 -0
  60. package/templates/base/src/ui/appComponents/customTextInput/index.tsx +143 -0
  61. package/templates/base/src/ui/components/.gitkeep +0 -0
  62. package/templates/base/src/ui/components/avatarBlock/index.tsx +42 -0
  63. package/templates/base/src/ui/components/backgroundGradient/index.tsx +24 -0
  64. package/templates/base/src/ui/components/errorFallback/index.tsx +40 -0
  65. package/templates/base/src/ui/iconComponents/IconFontAwesome6/index.tsx +22 -0
  66. package/templates/base/src/ui/iconComponents/IoniconsIcons/index.tsx +33 -0
  67. package/templates/base/src/ui/iconComponents/antDesignicons/index.tsx +22 -0
  68. package/templates/base/src/ui/iconComponents/featherIcons/index.tsx +22 -0
  69. package/templates/base/src/ui/iconComponents/materialCommunityIcons/index.tsx +24 -0
  70. package/templates/base/src/ui/iconComponents/materialIcons/index.tsx +23 -0
  71. package/templates/base/src/ui/iconComponents/octiconsIcons/index.tsx +23 -0
  72. package/templates/base/src/ui/theme/allFileStyles.ts +39 -0
  73. package/templates/base/src/ui/theme/colors.ts +54 -0
  74. package/templates/base/src/ui/theme/fonts.ts +3 -0
  75. package/templates/base/src/ui/theme/responsive.ts +27 -0
  76. package/templates/bottom-sheet/src/ui/appComponents/BottomSheetKeyboardAwareScrollView/index.tsx +111 -0
  77. package/templates/bottom-sheet/src/ui/appComponents/appBottomSheetBackdrop/index.tsx +27 -0
  78. package/templates/bottom-sheet/src/ui/appComponents/appBottomSheetScrollView/index.tsx +99 -0
  79. package/templates/bottom-sheet/src/ui/appComponents/appBottomSheetView/index.tsx +173 -0
  80. package/templates/bottom-sheet/src/ui/appComponents/customBottomSheetModal/index.tsx +17 -0
  81. package/templates/claude-command/init-app.md +41 -0
  82. package/templates/image-picker/media-constants.snippet.ts +18 -0
  83. package/templates/image-picker/src/core/services/PermissionService.ts +48 -0
@@ -0,0 +1,115 @@
1
+ import prompts from "prompts";
2
+ import { execa } from "execa";
3
+ import { log } from "./util.js";
4
+ const STRICT_BOOL_VARS = [
5
+ "EXPO_INCLUDE_BOTTOM_SHEET",
6
+ "EXPO_INCLUDE_IMAGE_PICKER",
7
+ ];
8
+ /**
9
+ * SHAPE-only validation of EXPO_* env vars. MUST be called from `src/index.ts`
10
+ * BEFORE `resolveTargetDir` (Phase 1) so invalid input throws BEFORE any fs
11
+ * mutation creates an orphan empty target dir.
12
+ *
13
+ * Per PLAN_V5.md Phase 2 step 2 (v5-r6).
14
+ */
15
+ export function validateEnvVars() {
16
+ const pm = process.env.EXPO_PACKAGE_MANAGER;
17
+ if (pm !== undefined && pm !== "" && pm !== "yarn" && pm !== "npm") {
18
+ throw new Error(`EXPO_PACKAGE_MANAGER: expected "yarn" or "npm", got "${pm}"`);
19
+ }
20
+ for (const key of STRICT_BOOL_VARS) {
21
+ const v = process.env[key];
22
+ if (v !== undefined && v !== "" && v !== "0" && v !== "1") {
23
+ throw new Error(`${key}: expected "0" or "1", got "${v}"`);
24
+ }
25
+ }
26
+ // EXPO_PRIMARY_FONT / EXPO_SECONDARY_FONT accept any string — no shape check.
27
+ }
28
+ function readBoolEnv(key) {
29
+ const v = process.env[key];
30
+ if (v === undefined || v === "")
31
+ return undefined;
32
+ return v === "1";
33
+ }
34
+ async function probeBin(bin, timeout = 3000) {
35
+ try {
36
+ const result = await execa(bin, ["--version"], { timeout, reject: false });
37
+ return result.exitCode === 0;
38
+ }
39
+ catch {
40
+ // ENOENT, TimeoutError, etc.
41
+ return false;
42
+ }
43
+ }
44
+ /**
45
+ * Resolve the package manager once and propagate the choice through every
46
+ * later install step so we never end up with both yarn.lock + package-lock.json.
47
+ *
48
+ * Order:
49
+ * 1. EXPO_PACKAGE_MANAGER override (already shape-validated by validateEnvVars).
50
+ * 2. yarn --version probe (3s timeout) → "yarn".
51
+ * 3. npm --version probe (3s timeout) → "npm".
52
+ * 4. Both missing → throw EARLY (before Phase 3 mutates fs).
53
+ */
54
+ export async function detectPackageManager() {
55
+ const override = process.env.EXPO_PACKAGE_MANAGER;
56
+ if (override === "yarn" || override === "npm")
57
+ return override;
58
+ if (await probeBin("yarn"))
59
+ return "yarn";
60
+ if (await probeBin("npm"))
61
+ return "npm";
62
+ throw new Error("Neither yarn nor npm available — install one before proceeding");
63
+ }
64
+ /**
65
+ * Gather all answers — env vars take precedence; missing values prompted from TTY.
66
+ * Non-TTY + missing answer → throw.
67
+ *
68
+ * NOTE: Fonts are intentionally hard-coded empty (Deviation #9). The generator
69
+ * empty-path produces `Fonts = {} as const` + drops `useFonts` sentinels →
70
+ * generated app has no font wiring. EXPO_PRIMARY_FONT / EXPO_SECONDARY_FONT
71
+ * env vars are silently ignored (still shape-validated by validateEnvVars).
72
+ */
73
+ export async function gatherAnswers() {
74
+ // Fonts disabled — see Deviation #9.
75
+ const primaryFont = "";
76
+ const secondaryFont = "";
77
+ // Only bottom-sheet + image-picker remain prompt-driven.
78
+ const envBottomSheet = readBoolEnv("EXPO_INCLUDE_BOTTOM_SHEET");
79
+ const envImagePicker = readBoolEnv("EXPO_INCLUDE_IMAGE_PICKER");
80
+ const tty = Boolean(process.stdin.isTTY);
81
+ const need = envBottomSheet === undefined || envImagePicker === undefined;
82
+ if (need && !tty) {
83
+ throw new Error("Missing required answers and stdin is not a TTY. Set " +
84
+ 'EXPO_INCLUDE_BOTTOM_SHEET + EXPO_INCLUDE_IMAGE_PICKER ("0" or "1").');
85
+ }
86
+ let bottomSheet;
87
+ if (envBottomSheet !== undefined) {
88
+ bottomSheet = envBottomSheet;
89
+ }
90
+ else {
91
+ const ans = await prompts({
92
+ type: "confirm",
93
+ name: "bottomSheet",
94
+ message: "Include bottom-sheet support?",
95
+ initial: false,
96
+ });
97
+ bottomSheet = Boolean(ans.bottomSheet);
98
+ }
99
+ let imagePicker;
100
+ if (envImagePicker !== undefined) {
101
+ imagePicker = envImagePicker;
102
+ }
103
+ else {
104
+ const ans = await prompts({
105
+ type: "confirm",
106
+ name: "imagePicker",
107
+ message: "Include image-picker support?",
108
+ initial: false,
109
+ });
110
+ imagePicker = Boolean(ans.imagePicker);
111
+ }
112
+ const packageManager = await detectPackageManager();
113
+ log.info(`Package manager: ${packageManager}`);
114
+ return { primaryFont, secondaryFont, bottomSheet, imagePicker, packageManager };
115
+ }
@@ -0,0 +1,43 @@
1
+ import path from "node:path";
2
+ import fs from "node:fs";
3
+ import { execa } from "execa";
4
+ import { fileExists, log } from "./util.js";
5
+ /**
6
+ * Wrap `create-expo-app` to scaffold the blank-typescript template into `dir`.
7
+ *
8
+ * Why `--no-install`:
9
+ * The CLI runs its own install pass later (Phase 7 `installNativeDeps`) using
10
+ * the resolved `packageManager` answer + `expo install` (so versions match the
11
+ * target SDK). Letting create-expo-app install separately would risk
12
+ * dual-lockfile + version drift.
13
+ *
14
+ * Why `--yes`:
15
+ * Forces a fresh fetch of `create-expo-app@latest`; bypasses any stale global
16
+ * install that could ship an older template.
17
+ *
18
+ * NOTE: `dir` is passed as the positional arg. create-expo-app interprets the
19
+ * final segment as the app name (`expo.name`); we still patch `expo.scheme`
20
+ * ourselves later (Phase 4 step 7) so the in-package name + scheme stay in sync.
21
+ */
22
+ export async function runCreateExpoApp(dir, _name) {
23
+ log.step(`Scaffolding blank-typescript template into ${dir}…`);
24
+ await execa("npx", [
25
+ "--yes",
26
+ "create-expo-app@latest",
27
+ dir,
28
+ "--template",
29
+ "blank-typescript",
30
+ "--no-install",
31
+ ], { stdio: "inherit" });
32
+ }
33
+ /**
34
+ * Delete `App.tsx` shipped by blank-typescript — collides with expo-router's
35
+ * auto-detection of `src/app/`. Idempotent (no-op if already absent).
36
+ */
37
+ export function cleanupBlankTemplate(target) {
38
+ const appTsx = path.join(target, "App.tsx");
39
+ if (fileExists(appTsx)) {
40
+ fs.rmSync(appTsx);
41
+ log.step("Removed blank-typescript App.tsx (replaced by expo-router src/app/).");
42
+ }
43
+ }
@@ -0,0 +1,19 @@
1
+ // Reads Phase 0 probe results from `docs/SDK_NOTES.md`. Each probe wrote a
2
+ // `KEY=VALUE` line; this exposes a Map-like lookup so CLI runtime branches
3
+ // don't need raw-text grep.
4
+ import fs from "node:fs";
5
+ export function readSDKNotes(filePath) {
6
+ const out = new Map();
7
+ if (!fs.existsSync(filePath)) {
8
+ // No probe file → CLI runs against an unprobed installation. All callers
9
+ // get conservative defaults (empty Map → `.get()` returns undefined).
10
+ return out;
11
+ }
12
+ const content = fs.readFileSync(filePath, "utf8");
13
+ for (const line of content.split("\n")) {
14
+ const m = line.match(/^([A-Z][A-Z0-9_]*)=(.*)$/);
15
+ if (m)
16
+ out.set(m[1], m[2]);
17
+ }
18
+ return out;
19
+ }
package/dist/util.js ADDED
@@ -0,0 +1,48 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import kleur from "kleur";
4
+ export function isDirEmpty(dir) {
5
+ try {
6
+ const entries = fs.readdirSync(dir);
7
+ // Treat directories with only dot-files (e.g. .git) as non-empty too — be strict.
8
+ return entries.length === 0;
9
+ }
10
+ catch (err) {
11
+ if (err.code === "ENOENT")
12
+ return true;
13
+ throw err;
14
+ }
15
+ }
16
+ export function ensureDir(dir) {
17
+ fs.mkdirSync(dir, { recursive: true });
18
+ }
19
+ export function dirExists(p) {
20
+ try {
21
+ return fs.statSync(p).isDirectory();
22
+ }
23
+ catch {
24
+ return false;
25
+ }
26
+ }
27
+ export function fileExists(p) {
28
+ try {
29
+ return fs.statSync(p).isFile();
30
+ }
31
+ catch {
32
+ return false;
33
+ }
34
+ }
35
+ export const log = {
36
+ info: (msg) => console.log(kleur.cyan("ℹ"), msg),
37
+ warn: (msg) => console.warn(kleur.yellow("⚠"), msg),
38
+ error: (msg) => console.error(kleur.red("✖"), msg),
39
+ success: (msg) => console.log(kleur.green("✔"), msg),
40
+ step: (msg) => console.log(kleur.magenta("›"), msg),
41
+ raw: (msg) => console.log(msg),
42
+ };
43
+ export function basename(p) {
44
+ return path.basename(p);
45
+ }
46
+ export function joinCwd(arg) {
47
+ return path.join(process.cwd(), arg);
48
+ }
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "codingpixel-expo-app",
3
+ "version": "0.1.0",
4
+ "description": "Opinionated Expo app scaffolder mirroring MyRoster conventions.",
5
+ "type": "module",
6
+ "bin": {
7
+ "codingpixel-expo": "bin/cli.js"
8
+ },
9
+ "files": [
10
+ "bin/",
11
+ "dist/",
12
+ "templates/",
13
+ "README.md",
14
+ "LICENSE"
15
+ ],
16
+ "engines": {
17
+ "node": ">=18"
18
+ },
19
+ "scripts": {
20
+ "build": "tsc -p tsconfig.json",
21
+ "dev": "tsx src/index.ts",
22
+ "test": "vitest run",
23
+ "audit:templates": "bash scripts/audit-templates.sh",
24
+ "prepublishOnly": "npm run build && npm run test && npm run audit:templates"
25
+ },
26
+ "keywords": [
27
+ "expo",
28
+ "scaffolder",
29
+ "create-expo-app",
30
+ "react-native"
31
+ ],
32
+ "author": "",
33
+ "license": "MIT",
34
+ "dependencies": {
35
+ "@babel/generator": "~7.29.1",
36
+ "@babel/parser": "~7.29.3",
37
+ "@babel/traverse": "~7.29.0",
38
+ "@babel/types": "~7.29.0",
39
+ "execa": "^9.6.1",
40
+ "fs-extra": "^11.3.5",
41
+ "kleur": "^4.1.5",
42
+ "prompts": "^2.4.2"
43
+ },
44
+ "devDependencies": {
45
+ "@types/babel__generator": "^7.27.0",
46
+ "@types/babel__traverse": "^7.28.0",
47
+ "@types/fs-extra": "^11.0.4",
48
+ "@types/node": "^25.6.2",
49
+ "@types/prompts": "^2.4.9",
50
+ "eslint": "^10.3.0",
51
+ "prettier": "^3.8.3",
52
+ "tsx": "^4.21.0",
53
+ "typescript": "5.5.4",
54
+ "vitest": "^4.1.5"
55
+ }
56
+ }
File without changes
File without changes
@@ -0,0 +1,8 @@
1
+ // Asset registry shim. Apps add real exports as they bundle .png / .ttf etc.
2
+ // Example:
3
+ // import notification from "./images/notification.png";
4
+ // export const Images = { notification };
5
+ //
6
+ // `@assets` resolves here via tsconfig path mapping.
7
+ export const Images = {} as Record<string, number>;
8
+ export {};
@@ -0,0 +1,39 @@
1
+ // Provider tree per PLAN_V5.md Phase 4 step 2. Sentinels are filled by Phase 6
2
+ // `patchLayout` based on the user's `useFonts` / `bottomSheet` answers.
3
+ import { ErrorBoundary } from "react-error-boundary";
4
+ import { GestureHandlerRootView } from "react-native-gesture-handler";
5
+ import { KeyboardProvider } from "react-native-keyboard-controller";
6
+ import { SafeAreaProvider } from "react-native-safe-area-context";
7
+ import { Provider } from "react-redux";
8
+ import { PersistGate } from "redux-persist/integration/react";
9
+ import { TanStackQueryProvider } from "@core/tanstack";
10
+ import { persistor, store } from "@redux/store";
11
+ import ErrorFallback from "@components/errorFallback";
12
+ import Routes from "./routes";
13
+ // @@USE_FONTS_IMPORT@@
14
+ // @@BOTTOM_SHEET_PROVIDER_IMPORT@@
15
+
16
+ export default function RootLayout() {
17
+ // @@USE_FONTS_HOOK@@
18
+ // @@USE_FONTS_GUARD@@
19
+
20
+ return (
21
+ <Provider store={store}>
22
+ <PersistGate loading={null} persistor={persistor}>
23
+ <TanStackQueryProvider>
24
+ <GestureHandlerRootView style={{ flex: 1 }}>
25
+ <SafeAreaProvider>
26
+ <KeyboardProvider>
27
+ {/* @@BOTTOM_SHEET_PROVIDER_OPEN@@ */}
28
+ <ErrorBoundary FallbackComponent={ErrorFallback}>
29
+ <Routes />
30
+ </ErrorBoundary>
31
+ {/* @@BOTTOM_SHEET_PROVIDER_CLOSE@@ */}
32
+ </KeyboardProvider>
33
+ </SafeAreaProvider>
34
+ </GestureHandlerRootView>
35
+ </TanStackQueryProvider>
36
+ </PersistGate>
37
+ </Provider>
38
+ );
39
+ }
@@ -0,0 +1,13 @@
1
+ import AppText from "@appComponents/appText";
2
+ import AppWrapper from "@appComponents/appWrapper";
3
+ import { Colors } from "@theme/colors";
4
+
5
+ export default function Home() {
6
+ return (
7
+ <AppWrapper>
8
+ <AppText size={20} color={Colors.BLACK}>
9
+ Hello from codingpixel-expo-app 👋
10
+ </AppText>
11
+ </AppWrapper>
12
+ );
13
+ }
@@ -0,0 +1,9 @@
1
+ import { Stack } from "expo-router";
2
+
3
+ export default function Routes() {
4
+ return (
5
+ <Stack screenOptions={{ headerShown: false }}>
6
+ <Stack.Screen name="index" />
7
+ </Stack>
8
+ );
9
+ }
File without changes
@@ -0,0 +1,6 @@
1
+ import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux";
2
+ import type { AppDispatch, AppState } from "./store";
3
+
4
+ export const useAppDispatch: () => AppDispatch = useDispatch;
5
+ export const useAppSelector: TypedUseSelectorHook<AppState> = useSelector;
6
+
@@ -0,0 +1,18 @@
1
+ import { createMMKV } from "react-native-mmkv";
2
+
3
+ const storage = createMMKV();
4
+
5
+ export const reduxStorage = {
6
+ setItem: (key: string, value: string) => {
7
+ storage.set(key, value);
8
+ return Promise.resolve(true);
9
+ },
10
+ getItem: (key: string) => {
11
+ const value = storage.getString(key);
12
+ return Promise.resolve(value);
13
+ },
14
+ removeItem: (key: string) => {
15
+ storage.remove(key);
16
+ return Promise.resolve();
17
+ },
18
+ };
@@ -0,0 +1,20 @@
1
+ import { combineReducers } from "@reduxjs/toolkit";
2
+ import { persistReducer } from "redux-persist";
3
+
4
+ import { reduxStorage } from "./mmkvStorage";
5
+ import { userReducer } from "./slices/userSlice";
6
+
7
+ export const rootReducer = combineReducers({
8
+ user: userReducer,
9
+ });
10
+
11
+ export type RootState = ReturnType<typeof rootReducer>;
12
+
13
+ const persistConfig = {
14
+ key: "root",
15
+ version: 1,
16
+ storage: reduxStorage,
17
+ whitelist: ["user"],
18
+ };
19
+
20
+ export const persistedReducer = persistReducer(persistConfig, rootReducer);
@@ -0,0 +1,31 @@
1
+ // Deviation #5 (docs/MIRROR_NOTES.md): minimal user shape per SPEC §6 ("dummy
2
+ // user shape"). Apps replace with their actual auth model.
3
+ import { createSlice, PayloadAction } from "@reduxjs/toolkit";
4
+
5
+ export type User = {
6
+ id: string | null;
7
+ name: string | null;
8
+ };
9
+
10
+ const initialState: User = { id: null, name: null };
11
+
12
+ const userSlice = createSlice({
13
+ name: "user",
14
+ initialState,
15
+ reducers: {
16
+ setUser: (state, action: PayloadAction<User>) => {
17
+ state.id = action.payload.id;
18
+ state.name = action.payload.name;
19
+ },
20
+ updateUser: (state, action: PayloadAction<Partial<User>>) => {
21
+ Object.assign(state, action.payload);
22
+ },
23
+ clearUser: (state) => {
24
+ state.id = null;
25
+ state.name = null;
26
+ },
27
+ },
28
+ });
29
+
30
+ export const { setUser, updateUser, clearUser } = userSlice.actions;
31
+ export default userSlice.reducer;
@@ -0,0 +1,28 @@
1
+ import { configureStore } from "@reduxjs/toolkit";
2
+ import { persistStore } from "redux-persist";
3
+
4
+ import { persistedReducer } from "./reducers";
5
+
6
+ export const store = configureStore({
7
+ reducer: persistedReducer,
8
+ middleware: (getDefaultMiddleware) =>
9
+ getDefaultMiddleware({
10
+ serializableCheck: {
11
+ ignoredActions: [
12
+ "persist/PERSIST",
13
+ "persist/REHYDRATE",
14
+ "persist/PAUSE",
15
+ "persist/FLUSH",
16
+ "persist/PURGE",
17
+ "persist/REGISTER",
18
+ ],
19
+ },
20
+ }),
21
+ });
22
+
23
+ export const persistor = persistStore(store);
24
+
25
+ export type AppStore = typeof store;
26
+ export type AppDispatch = AppStore["dispatch"];
27
+ export type AppState = ReturnType<AppStore["getState"]>;
28
+
File without changes
@@ -0,0 +1,93 @@
1
+ import type {
2
+ UseInfiniteQueryOptions,
3
+ UseInfiniteQueryResult,
4
+ UseMutationOptions,
5
+ UseMutationResult,
6
+ UseQueryOptions,
7
+ UseQueryResult,
8
+ } from "@tanstack/react-query";
9
+ import {
10
+ QueryClient,
11
+ QueryClientProvider,
12
+ useInfiniteQuery as useInfiniteQueryHook,
13
+ useMutation as useMutationHook,
14
+ useQueryClient as useQueryClientHook,
15
+ useQuery as useQueryHook,
16
+ } from "@tanstack/react-query";
17
+ import React from "react";
18
+
19
+ // Create a singleton QueryClient instance
20
+ // This ensures we don't create multiple instances
21
+ export const queryClient = new QueryClient({
22
+ defaultOptions: {
23
+ queries: {
24
+ retry: 1,
25
+ refetchOnWindowFocus: false,
26
+ staleTime: 5 * 60 * 1000, // 5 minutes
27
+ gcTime: 10 * 60 * 1000, // 10 minutes (formerly cacheTime)
28
+ },
29
+ mutations: {
30
+ retry: 1,
31
+ },
32
+ },
33
+ });
34
+
35
+ // QueryClientProvider component that wraps the app with the queryClient
36
+ export const TanStackQueryProvider: React.FC<{ children: React.ReactNode }> = ({
37
+ children,
38
+ }) => {
39
+ return (
40
+ <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
41
+ );
42
+ };
43
+
44
+ // Wrapper for useQuery to ensure single instance pattern
45
+ export function useQuery<
46
+ TQueryFnData = unknown,
47
+ TError = Error,
48
+ TData = TQueryFnData,
49
+ TQueryKey extends readonly unknown[] = readonly unknown[],
50
+ >(
51
+ options: UseQueryOptions<TQueryFnData, TError, TData, TQueryKey>,
52
+ ): UseQueryResult<TData, TError> {
53
+ return useQueryHook(options);
54
+ }
55
+
56
+ // Wrapper for useMutation to ensure single instance pattern
57
+ export function useMutation<
58
+ TData = unknown,
59
+ TError = Error,
60
+ TVariables = void,
61
+ TContext = unknown,
62
+ >(
63
+ options?: UseMutationOptions<TData, TError, TVariables, TContext>,
64
+ ): UseMutationResult<TData, TError, TVariables, TContext> {
65
+ return useMutationHook(options ?? {});
66
+ }
67
+
68
+ // Wrapper for useQueryClient to ensure single instance pattern
69
+ export function useQueryClient() {
70
+ return useQueryClientHook();
71
+ }
72
+
73
+ // Wrapper for useInfiniteQuery to ensure single instance pattern
74
+ export function useInfiniteQuery<
75
+ TQueryFnData = unknown,
76
+ TError = Error,
77
+ TData = TQueryFnData,
78
+ TQueryKey extends readonly unknown[] = readonly unknown[],
79
+ >(
80
+ options: UseInfiniteQueryOptions<TQueryFnData, TError, TData, TQueryKey>,
81
+ ): UseInfiniteQueryResult<TData, TError> {
82
+ return useInfiniteQueryHook(options);
83
+ }
84
+
85
+ // Export types for useQuery and useMutation
86
+ export type {
87
+ UseInfiniteQueryOptions,
88
+ UseInfiniteQueryResult,
89
+ UseMutationOptions,
90
+ UseMutationResult,
91
+ UseQueryOptions,
92
+ UseQueryResult,
93
+ };
@@ -0,0 +1,42 @@
1
+ export const TANSTACK_KEYS = {
2
+ // coach
3
+
4
+ // Dashboard
5
+ dashboard: ["coach", "dashboard"],
6
+ // Teams
7
+ teams: ["coach", "teams"],
8
+ teamDetails: (teamId: string) => ["coach", "teamDetails", teamId],
9
+ // Schedule
10
+ schedule: ["coach", "schedule"],
11
+ // Messages
12
+ messages: ["coach", "messages"],
13
+ messageDetails: (messageId: string) => ["coach", "messageDetails", messageId],
14
+ directChatInfo: (chatId: string) => ["directChatInfo", chatId],
15
+ directChatMessages: (chatId: string) => ["directChatMessages", chatId],
16
+ teamChatInfo: (teamId: string) => ["teamChatInfo", teamId],
17
+ teamChatMessages: (teamId: string) => ["teamChatMessages", teamId],
18
+ teamAnnouncements: (teamId: string) => ["teamAnnouncements", teamId],
19
+ // Payments
20
+ payments: ["coach", "payments"],
21
+ paymentsSummary: ["coach", "payments", "summary"],
22
+ paymentPlanDetail: (planId: string) => ["coach", "paymentPlanDetail", planId],
23
+ // Profile
24
+
25
+ // shared
26
+ playerDetails: (playerId: string) => ["shared", "playerDetails", playerId],
27
+
28
+ // parent
29
+ parentSchedule: ["parent", "schedule"],
30
+ parentDashboard: ["parent", "dashboard"],
31
+ myFamily: ["parent", "myFamily"],
32
+ programs: ["parent", "programs"],
33
+ programDetails: (id: string) => ["parent", "programDetails", id],
34
+ registration: ["parent", "registration"],
35
+ parentMessages: ["parent", "messages"],
36
+ parentMessageDetails: (messageId: string) => ["parent", "messageDetails", messageId],
37
+ parentPayments: ["parent", "payments"],
38
+ parentPaymentsSummary: ["parent", "payments", "summary"],
39
+ parentOrganizations: ["parent", "organizations"],
40
+ parentTeamInvitations: ["parent", "teamInvitations"],
41
+ parentTeamInvitationDetail: (id: string) => ["parent", "teamInvitationDetail", id],
42
+ };
@@ -0,0 +1,50 @@
1
+ import { clearUser } from "@redux/slices/userSlice";
2
+ import { store } from "@redux/store";
3
+ import axios, { AxiosInstance } from "axios";
4
+ import { Keyboard } from "react-native";
5
+ import { BASE_URL } from "./endpoints";
6
+
7
+ const HTTP_CLIENT: AxiosInstance = axios.create({
8
+ baseURL: BASE_URL,
9
+ timeout: 12000,
10
+ });
11
+
12
+ const initialConfig = () => {
13
+ setupAxios();
14
+ };
15
+
16
+ const setupAxios = () => {
17
+ HTTP_CLIENT.interceptors.request.use(
18
+ async (config: any) => {
19
+ Keyboard.dismiss();
20
+
21
+ const token = store.getState().user?.accessToken;
22
+ if (token) {
23
+ config.headers.Authorization = `Bearer ${token}`;
24
+ }
25
+ return config;
26
+ },
27
+ (err: any) => {
28
+ console.log("error in api: ", err);
29
+
30
+ Promise.reject(err);
31
+ },
32
+ );
33
+
34
+ HTTP_CLIENT.interceptors.response.use(
35
+ (response) => {
36
+ return response;
37
+ },
38
+ (err) => {
39
+ console.log("err in response", err, err?.message);
40
+
41
+ if (err?.response?.status == 401 || err?.status == 401) {
42
+ store.dispatch(clearUser());
43
+ }
44
+
45
+ return Promise.reject(err?.response?.data || err);
46
+ },
47
+ );
48
+ };
49
+
50
+ export { HTTP_CLIENT, initialConfig, setupAxios };
@@ -0,0 +1,11 @@
1
+ import { Platform } from "react-native";
2
+
3
+ export const ANDROID = Platform.OS === "android";
4
+ export const IOS = Platform.OS === "ios";
5
+
6
+ export const ImageSource = {
7
+ CAMERA: "camera" as const,
8
+ GALLERY: "gallery" as const,
9
+ };
10
+
11
+ // @@MEDIA_CONSTANTS@@