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.
- package/LICENSE +21 -0
- package/README.md +112 -0
- package/bin/cli.js +9 -0
- package/dist/babel.js +198 -0
- package/dist/bootstrap.js +54 -0
- package/dist/fonts.js +98 -0
- package/dist/index.js +120 -0
- package/dist/install.js +243 -0
- package/dist/overlay.js +141 -0
- package/dist/patch.js +212 -0
- package/dist/prompts.js +115 -0
- package/dist/scaffold.js +43 -0
- package/dist/sdkNotes.js +19 -0
- package/dist/util.js +48 -0
- package/package.json +56 -0
- package/templates/base/assets/fonts/.gitkeep +0 -0
- package/templates/base/assets/images/.gitkeep +0 -0
- package/templates/base/assets/index.ts +8 -0
- package/templates/base/src/app/_layout.tsx +39 -0
- package/templates/base/src/app/index.tsx +13 -0
- package/templates/base/src/app/routes.tsx +9 -0
- package/templates/base/src/core/hooks/.gitkeep +0 -0
- package/templates/base/src/core/redux/hooks.ts +6 -0
- package/templates/base/src/core/redux/mmkvStorage.ts +18 -0
- package/templates/base/src/core/redux/reducers.ts +20 -0
- package/templates/base/src/core/redux/slices/userSlice.ts +31 -0
- package/templates/base/src/core/redux/store.ts +28 -0
- package/templates/base/src/core/services/.gitkeep +0 -0
- package/templates/base/src/core/tanstack/index.tsx +93 -0
- package/templates/base/src/core/tanstack/tanstack-keys.ts +42 -0
- package/templates/base/src/core/utils/config.ts +50 -0
- package/templates/base/src/core/utils/constants.ts +11 -0
- package/templates/base/src/core/utils/endpoints.ts +29 -0
- package/templates/base/src/core/utils/types.ts +47 -0
- package/templates/base/src/core/utils/validation.ts +10 -0
- package/templates/base/src/features/.gitkeep +0 -0
- package/templates/base/src/ui/appComponents/AppRefreshControl/index.tsx +39 -0
- package/templates/base/src/ui/appComponents/appButton/index.tsx +122 -0
- package/templates/base/src/ui/appComponents/appColumnView/index.tsx +320 -0
- package/templates/base/src/ui/appComponents/appFlashList/index.tsx +191 -0
- package/templates/base/src/ui/appComponents/appFlatList/index.tsx +172 -0
- package/templates/base/src/ui/appComponents/appIcon/index.tsx +105 -0
- package/templates/base/src/ui/appComponents/appInput/index.tsx +226 -0
- package/templates/base/src/ui/appComponents/appKeyboardAvoidingView/index.tsx +168 -0
- package/templates/base/src/ui/appComponents/appKeyboardAwareScrollView/index.tsx +188 -0
- package/templates/base/src/ui/appComponents/appLogger/index.tsx +22 -0
- package/templates/base/src/ui/appComponents/appPressable/index.tsx +220 -0
- package/templates/base/src/ui/appComponents/appRowView/index.tsx +320 -0
- package/templates/base/src/ui/appComponents/appSafeAreaInsets/index.tsx +24 -0
- package/templates/base/src/ui/appComponents/appScrollView/index.tsx +166 -0
- package/templates/base/src/ui/appComponents/appSkeleton/index.tsx +22 -0
- package/templates/base/src/ui/appComponents/appStatusBar/index.tsx +8 -0
- package/templates/base/src/ui/appComponents/appTabHeader/index.tsx +111 -0
- package/templates/base/src/ui/appComponents/appText/index.tsx +258 -0
- package/templates/base/src/ui/appComponents/appTextWrapper/index.tsx +84 -0
- package/templates/base/src/ui/appComponents/appWrapper/index.tsx +56 -0
- package/templates/base/src/ui/appComponents/customActivityIndicator/index.tsx +34 -0
- package/templates/base/src/ui/appComponents/customGetPermissionModal/index.tsx +62 -0
- package/templates/base/src/ui/appComponents/customModal/index.tsx +26 -0
- package/templates/base/src/ui/appComponents/customTextInput/index.tsx +143 -0
- package/templates/base/src/ui/components/.gitkeep +0 -0
- package/templates/base/src/ui/components/avatarBlock/index.tsx +42 -0
- package/templates/base/src/ui/components/backgroundGradient/index.tsx +24 -0
- package/templates/base/src/ui/components/errorFallback/index.tsx +40 -0
- package/templates/base/src/ui/iconComponents/IconFontAwesome6/index.tsx +22 -0
- package/templates/base/src/ui/iconComponents/IoniconsIcons/index.tsx +33 -0
- package/templates/base/src/ui/iconComponents/antDesignicons/index.tsx +22 -0
- package/templates/base/src/ui/iconComponents/featherIcons/index.tsx +22 -0
- package/templates/base/src/ui/iconComponents/materialCommunityIcons/index.tsx +24 -0
- package/templates/base/src/ui/iconComponents/materialIcons/index.tsx +23 -0
- package/templates/base/src/ui/iconComponents/octiconsIcons/index.tsx +23 -0
- package/templates/base/src/ui/theme/allFileStyles.ts +39 -0
- package/templates/base/src/ui/theme/colors.ts +54 -0
- package/templates/base/src/ui/theme/fonts.ts +3 -0
- package/templates/base/src/ui/theme/responsive.ts +27 -0
- package/templates/bottom-sheet/src/ui/appComponents/BottomSheetKeyboardAwareScrollView/index.tsx +111 -0
- package/templates/bottom-sheet/src/ui/appComponents/appBottomSheetBackdrop/index.tsx +27 -0
- package/templates/bottom-sheet/src/ui/appComponents/appBottomSheetScrollView/index.tsx +99 -0
- package/templates/bottom-sheet/src/ui/appComponents/appBottomSheetView/index.tsx +173 -0
- package/templates/bottom-sheet/src/ui/appComponents/customBottomSheetModal/index.tsx +17 -0
- package/templates/claude-command/init-app.md +41 -0
- package/templates/image-picker/media-constants.snippet.ts +18 -0
- package/templates/image-picker/src/core/services/PermissionService.ts +48 -0
package/dist/prompts.js
ADDED
|
@@ -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
|
+
}
|
package/dist/scaffold.js
ADDED
|
@@ -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
|
+
}
|
package/dist/sdkNotes.js
ADDED
|
@@ -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
|
+
}
|
|
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@@
|