create-du-app 0.1.4 → 0.1.5

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 (77) hide show
  1. package/README.md +5 -2
  2. package/package.json +1 -1
  3. package/src/generate.js +15 -2
  4. package/src/index.js +7 -1
  5. package/templates/mobile/expo/.env.example +2 -2
  6. package/templates/mobile/expo/README.md +31 -3
  7. package/templates/mobile/expo/_package.json +13 -15
  8. package/templates/mobile/expo/app.json +10 -2
  9. package/templates/mobile/expo/index.js +2 -0
  10. package/templates/mobile/expo/src/app/app-provider.tsx +7 -3
  11. package/templates/mobile/expo/src/app/config/translation.ts +7 -3
  12. package/templates/mobile/expo/src/assets/i18n/en.json +19 -3
  13. package/templates/mobile/expo/src/assets/i18n/fr.json +19 -3
  14. package/templates/mobile/expo/src/core/components/forms/date-time-picker.modal.tsx +116 -0
  15. package/templates/mobile/expo/src/core/components/forms/hf-date-time.tsx +2 -10
  16. package/templates/mobile/expo/src/core/components/forms/hf-time-picker.tsx +2 -3
  17. package/templates/mobile/expo/src/core/components/screen/screen-container/screen-container.tsx +25 -29
  18. package/templates/mobile/expo/src/core/components/ui/app-image/app-image.tsx +16 -19
  19. package/templates/mobile/expo/src/core/components/ui/app-image/app-image.type.ts +6 -6
  20. package/templates/mobile/expo/src/core/components/ui/avatar-image/avatar-image.tsx +1 -1
  21. package/templates/mobile/expo/src/core/components/ui/image-slider/image-slider.tsx +3 -3
  22. package/templates/mobile/expo/src/core/components/ui/screen/screen-gradient.tsx +1 -1
  23. package/templates/mobile/expo/src/core/components/ui/skeleton/skeleton.tsx +1 -1
  24. package/templates/mobile/expo/src/core/services/api.service.ts +3 -3
  25. package/templates/mobile/expo/src/core/services/device-id.service.ts +16 -2
  26. package/templates/mobile/expo/src/core/utils/device-locale.util.ts +10 -8
  27. package/templates/mobile/expo/src/core/utils/image-picker.util.ts +37 -58
  28. package/templates/mobile/expo/src/core/utils/query-persister.util.ts +16 -21
  29. package/templates/mobile/expo/src/modules/home/home.screen.tsx +97 -20
  30. package/templates/mobile/rn/.bundle/config +2 -0
  31. package/templates/mobile/rn/.watchmanconfig +1 -0
  32. package/templates/mobile/rn/Gemfile +17 -0
  33. package/templates/mobile/rn/README.md +34 -2
  34. package/templates/mobile/rn/_package.json +2 -0
  35. package/templates/mobile/rn/android/app/build.gradle +126 -0
  36. package/templates/mobile/rn/android/app/debug.keystore +0 -0
  37. package/templates/mobile/rn/android/app/proguard-rules.pro +10 -0
  38. package/templates/mobile/rn/android/app/src/main/AndroidManifest.xml +27 -0
  39. package/templates/mobile/rn/android/app/src/main/java/com/dumobile/MainActivity.kt +22 -0
  40. package/templates/mobile/rn/android/app/src/main/java/com/dumobile/MainApplication.kt +27 -0
  41. package/templates/mobile/rn/android/app/src/main/res/drawable/rn_edit_text_material.xml +37 -0
  42. package/templates/mobile/rn/android/app/src/main/res/mipmap-hdpi/ic_launcher.png +0 -0
  43. package/templates/mobile/rn/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png +0 -0
  44. package/templates/mobile/rn/android/app/src/main/res/mipmap-mdpi/ic_launcher.png +0 -0
  45. package/templates/mobile/rn/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png +0 -0
  46. package/templates/mobile/rn/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png +0 -0
  47. package/templates/mobile/rn/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png +0 -0
  48. package/templates/mobile/rn/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png +0 -0
  49. package/templates/mobile/rn/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png +0 -0
  50. package/templates/mobile/rn/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png +0 -0
  51. package/templates/mobile/rn/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png +0 -0
  52. package/templates/mobile/rn/android/app/src/main/res/values/strings.xml +3 -0
  53. package/templates/mobile/rn/android/app/src/main/res/values/styles.xml +9 -0
  54. package/templates/mobile/rn/android/build.gradle +21 -0
  55. package/templates/mobile/rn/android/gradle/wrapper/gradle-wrapper.jar +0 -0
  56. package/templates/mobile/rn/android/gradle/wrapper/gradle-wrapper.properties +7 -0
  57. package/templates/mobile/rn/android/gradle.properties +44 -0
  58. package/templates/mobile/rn/android/gradlew +248 -0
  59. package/templates/mobile/rn/android/gradlew.bat +98 -0
  60. package/templates/mobile/rn/android/settings.gradle +21 -0
  61. package/templates/mobile/rn/app.json +1 -1
  62. package/templates/mobile/rn/index.js +2 -0
  63. package/templates/mobile/rn/ios/.xcode.env +11 -0
  64. package/templates/mobile/rn/ios/DuMobile/AppDelegate.swift +48 -0
  65. package/templates/mobile/rn/ios/DuMobile/Images.xcassets/AppIcon.appiconset/Contents.json +53 -0
  66. package/templates/mobile/rn/ios/DuMobile/Images.xcassets/Contents.json +6 -0
  67. package/templates/mobile/rn/ios/DuMobile/Info.plist +59 -0
  68. package/templates/mobile/rn/ios/DuMobile/LaunchScreen.storyboard +47 -0
  69. package/templates/mobile/rn/ios/DuMobile/PrivacyInfo.xcprivacy +37 -0
  70. package/templates/mobile/rn/ios/DuMobile.xcodeproj/project.pbxproj +475 -0
  71. package/templates/mobile/rn/ios/DuMobile.xcodeproj/xcshareddata/xcschemes/DuMobile.xcscheme +88 -0
  72. package/templates/mobile/rn/ios/Podfile +34 -0
  73. package/templates/mobile/rn/src/app/app-provider.tsx +19 -14
  74. package/templates/mobile/rn/src/app/config/translation.ts +3 -0
  75. package/templates/mobile/rn/src/assets/i18n/en.json +13 -3
  76. package/templates/mobile/rn/src/assets/i18n/fr.json +13 -3
  77. package/templates/mobile/rn/src/modules/home/home.screen.tsx +53 -19
package/README.md CHANGED
@@ -19,10 +19,13 @@ You'll be asked:
19
19
  2. **Select the groups** → press **`Space`** to tick Mobile / Frontend / Backend, then **`Enter`**
20
20
  3. **Pick one technology** per group → **↑ / ↓** then **`Enter`**
21
21
 
22
- Then:
22
+ Then bootstrap it — one command installs deps, builds `@repo/shared`, and
23
+ verifies every app can import it:
23
24
 
24
25
  ```bash
25
- cd my-shop && pnpm install
26
+ cd my-shop
27
+ pnpm bootstrap
28
+ pnpm dev
26
29
  ```
27
30
 
28
31
  Done. 🎉
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-du-app",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "CLI generator: pick templates (Mobile/FE/BE) and scaffold a product monorepo",
5
5
  "type": "module",
6
6
  "bin": {
package/src/generate.js CHANGED
@@ -81,7 +81,19 @@ async function copyTemplate(srcDir, destDir, projectName) {
81
81
  // ---------------------------------------------------------------------------
82
82
 
83
83
  // Build the project's root package.json (pnpm: NO "workspaces" field).
84
- function buildRootPackageJson(projectName, hasShared) {
84
+ function buildRootPackageJson(projectName, hasShared, createdAppDirs = []) {
85
+ // Per-app convenience scripts (life-master style): build @repo/shared first
86
+ // (if it exists), then run that app's dev server. Keyed by the app folder
87
+ // name, filtered to the app's package (e.g. `pnpm mobile` -> runs apps/mobile).
88
+ const sharedPrefix = hasShared
89
+ ? `turbo run build --filter=${SHARED_PACKAGE_NAME} && `
90
+ : '';
91
+ const appScripts = {};
92
+ for (const dir of createdAppDirs) {
93
+ const app = dir.split('/').pop(); // 'apps/mobile' -> 'mobile'
94
+ appScripts[app] = `${sharedPrefix}turbo run dev --filter=${projectName}-${app}`;
95
+ }
96
+
85
97
  return {
86
98
  name: projectName,
87
99
  version: '0.1.0',
@@ -98,6 +110,7 @@ function buildRootPackageJson(projectName, hasShared) {
98
110
  ...(hasShared
99
111
  ? { 'shared:build': `turbo run build --filter=${SHARED_PACKAGE_NAME}` }
100
112
  : {}),
113
+ ...appScripts,
101
114
  },
102
115
  devDependencies: {
103
116
  turbo: '^2.3.3',
@@ -333,7 +346,7 @@ export async function generate(plan, repoRoot, cwd = process.cwd()) {
333
346
  // 3) Generate the root config files dynamically.
334
347
  await fse.writeJson(
335
348
  path.join(projectRoot, 'package.json'),
336
- buildRootPackageJson(projectName, hasShared),
349
+ buildRootPackageJson(projectName, hasShared, createdAppDirs),
337
350
  { spaces: 2 },
338
351
  );
339
352
  logs.push('✓ package.json');
package/src/index.js CHANGED
@@ -168,7 +168,13 @@ async function main() {
168
168
  console.log('');
169
169
 
170
170
  const rel = path.relative(process.cwd(), projectRoot) || projectRoot;
171
- const msg = `Done! Next:\n\n cd ${rel} && pnpm install\n`;
171
+ // `pnpm bootstrap` is the one-command setup: it installs deps, builds
172
+ // @repo/shared, and verifies each app resolves it. Then `pnpm dev`.
173
+ const msg =
174
+ `Done! Next:\n\n` +
175
+ ` cd ${rel}\n` +
176
+ ` pnpm bootstrap # install deps + build @repo/shared + verify\n` +
177
+ ` pnpm dev # start the dev servers\n`;
172
178
  // outro() (clack) only looks good in a TTY; print plainly in non-interactive mode.
173
179
  if (process.stdout.isTTY && !hasSelectionFlags(args)) outro(msg);
174
180
  else console.log(msg);
@@ -1,5 +1,5 @@
1
- # Copy to .env and fill in. Read at build time by react-native-config.
1
+ # Copy to .env and fill in. Expo inlines any EXPO_PUBLIC_* var at build time.
2
2
  # NEVER commit the real .env — only this example.
3
3
 
4
4
  # Base URL of your API (include trailing slash).
5
- BASE_URL=https://api.example.com/
5
+ EXPO_PUBLIC_BASE_URL=https://api.example.com/
@@ -19,7 +19,7 @@ src/
19
19
  | Area | Where |
20
20
  |------|-------|
21
21
  | API | `services/api.service.ts` (axios + auth header + device-id + 401 session handling), `api/example.api.ts` (React Query pattern) |
22
- | Data | `utils/query-client.util.ts`, `utils/query-persister.util.ts` (MMKV offline cache) |
22
+ | Data | `utils/query-client.util.ts`, `utils/query-persister.util.ts` (AsyncStorage offline cache) |
23
23
  | Theme | `theme/` — context + `Colors` design tokens (`useTheme`, `useThemedStyles`) |
24
24
  | UI kit | `components/ui/*` — button, text-field/area, checkbox, radio, toggle, modal, bottom-sheet, header, tabs, otp-input, search-box, skeleton, app-image, image-slider… |
25
25
  | Forms | `components/forms/*` — react-hook-form bindings over the DS fields |
@@ -33,14 +33,42 @@ package exists; the CLI wires `"@repo/shared": "workspace:*"` automatically.
33
33
 
34
34
  ## Getting started
35
35
 
36
+ > [!TIP]
37
+ > Every native dependency in this starter ships inside **Expo Go**, so you can
38
+ > run it by scanning the QR code — **no dev build / Xcode / Android Studio
39
+ > needed**. (See [Expo Go compatibility](#expo-go-compatibility) for how.)
40
+
36
41
  ```bash
37
42
  pnpm bootstrap # install + build @repo/shared
38
43
  pnpm --filter {{PROJECT_NAME}}-mobile typecheck # verify
39
- pnpm --filter {{PROJECT_NAME}}-mobile start # expo dev server
44
+ pnpm --filter {{PROJECT_NAME}}-mobile start # expo start — scan QR in Expo Go
40
45
  ```
41
46
 
47
+ Press `i` (iOS simulator) / `a` (Android emulator) in the Metro terminal, or scan
48
+ the QR with the **Expo Go** app on a physical device.
49
+
42
50
  Add an icon: drop `name.svg` in `src/assets/svgs/` then `pnpm --filter {{PROJECT_NAME}}-mobile sync-svgs`.
43
51
 
44
- Native folders (`ios/`, `android/`) are git-ignored — run `expo prebuild` to generate them.
52
+ ## Expo Go compatibility
53
+
54
+ This starter deliberately uses only libraries bundled in Expo Go, so the standard
55
+ QR-code workflow works out of the box. Where the original production app used a
56
+ bare-RN native module, the Expo equivalent is used instead:
57
+
58
+ | Need | Library used |
59
+ |------|--------------|
60
+ | Images (cache, prefetch) | `expo-image` |
61
+ | Pick / capture photos | `expo-image-picker` |
62
+ | Date / time picker | `@react-native-community/datetimepicker` |
63
+ | Gradients | `expo-linear-gradient` |
64
+ | Device locale | `expo-localization` |
65
+ | Device id | `expo-application` (+ `expo-crypto` fallback) |
66
+ | Offline query cache | `@react-native-async-storage/async-storage` |
67
+ | Env vars | `process.env.EXPO_PUBLIC_*` (set in `.env`) |
68
+ | Keyboard avoidance | React Native `KeyboardAvoidingView` |
69
+
70
+ > Need a bare-RN native module later (e.g. MMKV, a custom SDK)? Run
71
+ > `npx expo prebuild` + `expo run:ios` / `run:android` to switch to a development
72
+ > build. Until then, plain `expo start` + Expo Go is all you need.
45
73
 
46
74
  > Before shipping: set `name`, `slug`, `scheme`, and the iOS/Android bundle ids in `app.json`.
@@ -4,6 +4,7 @@
4
4
  "private": true,
5
5
  "main": "index.js",
6
6
  "scripts": {
7
+ "dev": "expo start",
7
8
  "start": "expo start",
8
9
  "android": "expo run:android",
9
10
  "ios": "expo run:ios",
@@ -15,38 +16,35 @@
15
16
  },
16
17
  "dependencies": {
17
18
  "@hookform/error-message": "2.0.0",
18
- "@react-native-async-storage/async-storage": "^2.1.2",
19
- "@react-native-community/netinfo": "^12.0.1",
19
+ "@react-native-async-storage/async-storage": "2.2.0",
20
+ "@react-native-community/netinfo": "12.0.1",
20
21
  "@react-navigation/bottom-tabs": "^7.12.0",
21
22
  "@react-navigation/native": "^7.0.13",
23
+ "@react-native-community/datetimepicker": "9.1.0",
22
24
  "@react-navigation/native-stack": "^7.1.14",
23
25
  "@tanstack/react-query": "^5.101.0",
24
26
  "@tanstack/react-query-persist-client": "^5.101.0",
25
27
  "axios": "^1.7.9",
26
28
  "expo": "~56.0.6",
29
+ "expo-application": "~56.0.3",
30
+ "expo-crypto": "~56.0.4",
31
+ "expo-image": "~56.0.11",
32
+ "expo-image-picker": "~56.0.18",
33
+ "expo-linear-gradient": "~56.0.4",
34
+ "expo-localization": "~56.0.6",
27
35
  "i18next": "^21.5.4",
28
36
  "lodash": "^4.17.21",
29
- "lottie-react-native": "^7.3.4",
37
+ "lottie-react-native": "~7.3.4",
30
38
  "qs": "^6.10.1",
31
39
  "react": "19.2.3",
32
40
  "react-hook-form": "^7.53.2",
33
41
  "react-i18next": "^11.14.3",
34
42
  "react-native": "0.85.3",
35
- "react-native-config": "^1.6.1",
36
- "react-native-date-picker": "5.0.12",
37
- "react-native-device-info": "^15.0.2",
38
- "react-native-fast-image": "^8.6.3",
39
43
  "react-native-gesture-handler": "~2.31.1",
40
- "react-native-image-picker": "^8.2.1",
41
- "react-native-keyboard-controller": "^1.21.6",
42
- "react-native-linear-gradient": "^2.8.3",
43
- "react-native-localize": "^3.7.0",
44
- "react-native-mmkv": "^4.3.1",
45
- "react-native-nitro-modules": "^0.35.9",
46
44
  "react-native-reanimated": "4.3.1",
47
- "react-native-safe-area-context": "^5.7.0",
45
+ "react-native-safe-area-context": "~5.7.0",
48
46
  "react-native-screens": "4.25.2",
49
- "react-native-svg": "^15.15.4",
47
+ "react-native-svg": "15.15.4",
50
48
  "react-native-toast-message": "^2.2.1",
51
49
  "react-native-worklets": "0.8.3",
52
50
  "yup": "^0.32.11",
@@ -9,10 +9,18 @@
9
9
  "scheme": "myapp",
10
10
  "ios": {
11
11
  "supportsTablet": true,
12
- "bundleIdentifier": "com.company.myapp"
12
+ "bundleIdentifier": "com.company.myapp",
13
+ "infoPlist": {
14
+ "NSPhotoLibraryUsageDescription": "Allow $(PRODUCT_NAME) to access your photos.",
15
+ "NSCameraUsageDescription": "Allow $(PRODUCT_NAME) to use the camera."
16
+ }
13
17
  },
14
18
  "android": {
15
- "package": "com.company.myapp"
19
+ "package": "com.company.myapp",
20
+ "permissions": [
21
+ "android.permission.CAMERA",
22
+ "android.permission.READ_MEDIA_IMAGES"
23
+ ]
16
24
  }
17
25
  }
18
26
  }
@@ -1,3 +1,5 @@
1
+ // Must be the very first import (react-native-gesture-handler requirement).
2
+ import 'react-native-gesture-handler';
1
3
  import { registerRootComponent } from 'expo';
2
4
  import AppProvider from './src/app/app-provider';
3
5
 
@@ -5,7 +5,8 @@ import { I18nextProvider } from 'react-i18next';
5
5
  import { onlineManager } from '@tanstack/react-query';
6
6
  import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client';
7
7
  import NetInfo from '@react-native-community/netinfo';
8
- import { queryClient, mmkvQueryPersister } from '@src/core/utils';
8
+ import Toast from 'react-native-toast-message';
9
+ import { queryClient, queryPersister, toastConfig } from '@src/core/utils';
9
10
  import { ThemeProvider } from '@src/core/theme';
10
11
  import { i18n } from './config/translation';
11
12
  import { App } from './App';
@@ -16,17 +17,20 @@ onlineManager.setEventListener(setOnline =>
16
17
  );
17
18
 
18
19
  // The provider tree. Order: gesture root → persisted query cache → i18n →
19
- // safe-area → theme → app UI.
20
+ // safe-area → theme → app UI. <Toast> is mounted last so it overlays all.
21
+ // Keyboard handling uses React Native's built-in KeyboardAvoidingView (in
22
+ // ScreenContainer) so the app runs in Expo Go without a custom dev build.
20
23
  export default function AppProvider() {
21
24
  return (
22
25
  <GestureHandlerRootView style={{ flex: 1 }}>
23
26
  <PersistQueryClientProvider
24
27
  client={queryClient}
25
- persistOptions={{ persister: mmkvQueryPersister }}>
28
+ persistOptions={{ persister: queryPersister }}>
26
29
  <I18nextProvider i18n={i18n}>
27
30
  <SafeAreaProvider>
28
31
  <ThemeProvider>
29
32
  <App />
33
+ <Toast config={toastConfig} />
30
34
  </ThemeProvider>
31
35
  </SafeAreaProvider>
32
36
  </I18nextProvider>
@@ -1,4 +1,4 @@
1
- import { findBestLanguageTag } from 'react-native-localize';
1
+ import { getLocales } from 'expo-localization';
2
2
  import { Translator } from '@src/core/utils';
3
3
  import en from '@src/assets/i18n/en.json';
4
4
  import fr from '@src/assets/i18n/fr.json';
@@ -9,15 +9,19 @@ const resources = {
9
9
  };
10
10
 
11
11
  const fallbackLng = 'en';
12
- const best = findBestLanguageTag(Object.keys(resources));
12
+ // expo-localization reads the OS preferred-languages list (ships in Expo Go).
13
+ const deviceLanguage = getLocales()[0]?.languageCode ?? fallbackLng;
13
14
 
14
15
  // Initialize i18next once and expose the instance. The provider mounts it in
15
16
  // app-provider.tsx; the active language can be changed via Translator.changeLanguages.
16
17
  export const i18n = Translator.setup({
17
18
  resources,
18
- lng: best?.languageTag?.split('-')[0] ?? fallbackLng,
19
+ lng: deviceLanguage in resources ? deviceLanguage : fallbackLng,
19
20
  fallbackLng,
20
21
  interpolation: { escapeValue: false },
22
+ // RN's JS engine lacks full Intl.PluralRules — use i18next's v3 plural format
23
+ // so it doesn't warn and fall back at runtime.
24
+ compatibilityJSON: 'v3',
21
25
  });
22
26
 
23
27
  // Hook persisted-language restore here if you store a user preference.
@@ -1,12 +1,28 @@
1
1
  {
2
2
  "home": {
3
3
  "title": "Home",
4
- "greeting": "Hello, {{name}}",
5
- "loadError": "Failed to load.",
6
- "count": "{{n}} item(s)"
4
+ "subtitle": "Your company mobile starter — built on the shared monorepo core.",
5
+ "features": {
6
+ "nav": { "title": "Navigation", "desc": "React Navigation — typed native-stack + bottom tabs." },
7
+ "data": { "title": "Networking", "desc": "Axios client + React Query, offline cache via MMKV." },
8
+ "i18n": { "title": "i18n", "desc": "react-i18next with device locale (en / fr)." },
9
+ "theme": { "title": "Theming", "desc": "Theme context + design tokens (dark / light)." },
10
+ "forms": { "title": "Forms", "desc": "react-hook-form bindings over the design-system fields." },
11
+ "ui": { "title": "UI kit", "desc": "Buttons, inputs, modals, tabs, skeletons and more." },
12
+ "state": { "title": "State", "desc": "Zustand stores (auth, loading) + AsyncStorage." },
13
+ "shared": { "title": "@repo/shared", "desc": "Types, enums and API contracts shared across apps." }
14
+ },
15
+ "footer": "Edit src/modules/home to make it yours.",
16
+ "language": "Language",
17
+ "author": "Created by Long Mobile",
18
+ "loadError": "Failed to load."
7
19
  },
8
20
  "profile": {
9
21
  "title": "Profile",
10
22
  "signOut": "Sign out"
23
+ },
24
+ "common": {
25
+ "confirm": "Confirm",
26
+ "cancel": "Cancel"
11
27
  }
12
28
  }
@@ -1,12 +1,28 @@
1
1
  {
2
2
  "home": {
3
3
  "title": "Accueil",
4
- "greeting": "Bonjour, {{name}}",
5
- "loadError": "Échec du chargement.",
6
- "count": "{{n}} élément(s)"
4
+ "subtitle": "Votre starter mobile d'entreprise — basé sur le cœur partagé du monorepo.",
5
+ "features": {
6
+ "nav": { "title": "Navigation", "desc": "React Navigation — native-stack + onglets typés." },
7
+ "data": { "title": "Réseau", "desc": "Client Axios + React Query, cache hors-ligne via MMKV." },
8
+ "i18n": { "title": "i18n", "desc": "react-i18next avec la langue de l'appareil (en / fr)." },
9
+ "theme": { "title": "Thème", "desc": "Contexte de thème + design tokens (sombre / clair)." },
10
+ "forms": { "title": "Formulaires", "desc": "react-hook-form sur les champs du design-system." },
11
+ "ui": { "title": "Kit UI", "desc": "Boutons, champs, modales, onglets, skeletons, etc." },
12
+ "state": { "title": "State", "desc": "Stores Zustand (auth, loading) + AsyncStorage." },
13
+ "shared": { "title": "@repo/shared", "desc": "Types, enums et contrats d'API partagés entre apps." }
14
+ },
15
+ "footer": "Modifiez src/modules/home pour l'adapter.",
16
+ "language": "Langue",
17
+ "author": "Créé par Long Mobile",
18
+ "loadError": "Échec du chargement."
7
19
  },
8
20
  "profile": {
9
21
  "title": "Profil",
10
22
  "signOut": "Se déconnecter"
23
+ },
24
+ "common": {
25
+ "confirm": "Confirmer",
26
+ "cancel": "Annuler"
11
27
  }
12
28
  }
@@ -0,0 +1,116 @@
1
+ import React, { useEffect, useState } from 'react';
2
+ import { Modal, Platform, StyleSheet, TouchableOpacity, View } from 'react-native';
3
+ import DateTimePicker, {
4
+ DateTimePickerAndroid,
5
+ } from '@react-native-community/datetimepicker';
6
+ import { useTranslation } from 'react-i18next';
7
+ import { ThemeColors, useThemedStyles } from '@src/core/theme';
8
+ import { Font, fontSize } from '../../utils';
9
+ import { Label } from '../ui';
10
+
11
+ type Props = {
12
+ open: boolean;
13
+ date: Date;
14
+ mode: 'date' | 'time';
15
+ maximumDate?: Date;
16
+ onConfirm: (date: Date) => void;
17
+ onCancel: () => void;
18
+ };
19
+
20
+ /**
21
+ * Cross-platform date/time picker built on @react-native-community/datetimepicker
22
+ * (ships in Expo Go — no dev build needed). On Android it opens the native OS
23
+ * dialog imperatively; on iOS it renders a spinner inside a bottom-sheet modal
24
+ * with Confirm / Cancel actions.
25
+ */
26
+ export const DateTimePickerModal = ({
27
+ open,
28
+ date,
29
+ mode,
30
+ maximumDate,
31
+ onConfirm,
32
+ onCancel,
33
+ }: Props) => {
34
+ const { t } = useTranslation();
35
+ const styles = useThemedStyles(makeStyles);
36
+ const [temp, setTemp] = useState(date);
37
+
38
+ useEffect(() => {
39
+ if (open) setTemp(date);
40
+ }, [open, date]);
41
+
42
+ // Android has no in-tree picker view — open the OS dialog when `open` flips on.
43
+ useEffect(() => {
44
+ if (Platform.OS !== 'android' || !open) return;
45
+ DateTimePickerAndroid.open({
46
+ value: date,
47
+ mode,
48
+ maximumDate,
49
+ onChange: (event, selected) => {
50
+ if (event.type === 'set' && selected) onConfirm(selected);
51
+ else onCancel();
52
+ },
53
+ });
54
+ // Reacts only to `open` flipping true; date/mode are read at open time.
55
+ // eslint-disable-next-line react-hooks/exhaustive-deps
56
+ }, [open]);
57
+
58
+ if (Platform.OS === 'android') return null;
59
+
60
+ return (
61
+ <Modal visible={open} transparent animationType="slide" onRequestClose={onCancel}>
62
+ <View style={styles.backdrop}>
63
+ <View style={styles.sheet}>
64
+ <View style={styles.actions}>
65
+ <TouchableOpacity onPress={onCancel} hitSlop={8}>
66
+ <Label style={styles.cancel} value={t('common.cancel')} />
67
+ </TouchableOpacity>
68
+ <TouchableOpacity onPress={() => onConfirm(temp)} hitSlop={8}>
69
+ <Label style={styles.confirm} value={t('common.confirm')} />
70
+ </TouchableOpacity>
71
+ </View>
72
+ <DateTimePicker
73
+ value={temp}
74
+ mode={mode}
75
+ display="spinner"
76
+ maximumDate={maximumDate}
77
+ onChange={(_, selected) => selected && setTemp(selected)}
78
+ />
79
+ </View>
80
+ </View>
81
+ </Modal>
82
+ );
83
+ };
84
+
85
+ export default DateTimePickerModal;
86
+
87
+ const makeStyles = (c: ThemeColors) =>
88
+ StyleSheet.create({
89
+ backdrop: {
90
+ flex: 1,
91
+ justifyContent: 'flex-end',
92
+ backgroundColor: 'rgba(0,0,0,0.4)',
93
+ },
94
+ sheet: {
95
+ backgroundColor: c.bg_elevation_level_1_normal,
96
+ paddingBottom: 24,
97
+ },
98
+ actions: {
99
+ flexDirection: 'row',
100
+ justifyContent: 'space-between',
101
+ paddingHorizontal: 16,
102
+ paddingVertical: 12,
103
+ borderBottomWidth: StyleSheet.hairlineWidth,
104
+ borderBottomColor: c.bd_neutral_faded,
105
+ },
106
+ cancel: {
107
+ fontFamily: Font.rethinkSansRegular,
108
+ fontSize: fontSize(16),
109
+ color: c.fg_neutral_faded,
110
+ },
111
+ confirm: {
112
+ fontFamily: Font.rethinkSansBold,
113
+ fontSize: fontSize(16),
114
+ color: c.fg_neutral_normal,
115
+ },
116
+ });
@@ -1,7 +1,6 @@
1
1
  import { ErrorMessage } from '@hookform/error-message';
2
2
  import React, { FC, useMemo, useState } from 'react';
3
3
  import { useController, useFormContext, useFormState } from 'react-hook-form';
4
- import { useTranslation } from 'react-i18next';
5
4
  import {
6
5
  StyleProp,
7
6
  StyleSheet,
@@ -11,7 +10,7 @@ import {
11
10
  ViewProps,
12
11
  ViewStyle,
13
12
  } from 'react-native';
14
- import DatePicker from 'react-native-date-picker';
13
+ import { DateTimePickerModal } from './date-time-picker.modal';
15
14
  import {
16
15
  Font,
17
16
  fontSize,
@@ -23,7 +22,6 @@ import { ThemeColors, useThemedStyles } from '@src/core/theme';
23
22
  import { Label } from '../ui';
24
23
  import { SvgProps } from 'react-native-svg';
25
24
  import { IconCalendar } from '@src/assets/svgs';
26
- import { i18n } from '@src/app/config/translation';
27
25
 
28
26
  type Props = ViewProps & {
29
27
  containerStyle?: StyleProp<ViewStyle>;
@@ -99,7 +97,6 @@ const formatDate = (date: Date, yearOnly?: boolean) => {
99
97
  };
100
98
 
101
99
  const HFDateTime = (props: Props) => {
102
- const { t } = useTranslation();
103
100
  const styles = useThemedStyles(makeStyles);
104
101
  const {
105
102
  containerStyle = {},
@@ -220,16 +217,11 @@ const HFDateTime = (props: Props) => {
220
217
  }}
221
218
  />
222
219
  </View>
223
- <DatePicker
224
- modal
220
+ <DateTimePickerModal
225
221
  mode="date"
226
222
  open={open}
227
223
  date={date}
228
224
  maximumDate={new Date()}
229
- locale={i18n.language}
230
- title={t('common.selectDate')}
231
- confirmText={t('common.confirm')}
232
- cancelText={t('common.cancel')}
233
225
  onConfirm={handleConfirm}
234
226
  onCancel={() => setOpen(false)}
235
227
  />
@@ -6,7 +6,7 @@ import {
6
6
  TouchableOpacity,
7
7
  ViewStyle,
8
8
  } from 'react-native';
9
- import DatePicker from 'react-native-date-picker';
9
+ import { DateTimePickerModal } from './date-time-picker.modal';
10
10
  import { Font, fontSize, horizontalScale, verticalScale, Radius } from '../../utils';
11
11
  import { ThemeColors, useThemedStyles } from '@src/core/theme';
12
12
  import { Label } from '../ui';
@@ -80,8 +80,7 @@ const HFTimePicker = (props: Props) => {
80
80
  value={field.value || placeholder}
81
81
  />
82
82
  </TouchableOpacity>
83
- <DatePicker
84
- modal
83
+ <DateTimePickerModal
85
84
  mode="time"
86
85
  open={open}
87
86
  date={time}
@@ -11,6 +11,7 @@ import React, { PropsWithChildren, ReactNode } from 'react';
11
11
  import {
12
12
  Image,
13
13
  ImageSourcePropType,
14
+ KeyboardAvoidingView,
14
15
  NativeScrollEvent,
15
16
  NativeSyntheticEvent,
16
17
  StatusBar,
@@ -20,7 +21,6 @@ import {
20
21
  ViewStyle,
21
22
  } from 'react-native';
22
23
  import { ScrollView } from 'react-native-gesture-handler';
23
- import { KeyboardAwareScrollView } from 'react-native-keyboard-controller';
24
24
  import { Edge, SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
25
25
  import { ImgScreenBgGradian } from '@src/assets/Images';
26
26
  import Header from '../../ui/header/header';
@@ -43,15 +43,14 @@ interface Props {
43
43
  /** Keyboard-aware scroll for forms. @default true */
44
44
  isForm?: boolean;
45
45
  /**
46
- * Extra breathing space (px) kept between the focused input and the top of
47
- * the keyboard while `isForm`. The scroll view animates the whole form up by
48
- * this much past the keyboard. @default Spacing.spacing_2xl (32)
46
+ * Extra breathing space (px) kept above the keyboard while `isForm` passed
47
+ * to KeyboardAvoidingView as `keyboardVerticalOffset`.
48
+ * @default Spacing.spacing_2xl (32)
49
49
  */
50
50
  bottomOffset?: number;
51
51
  /**
52
- * Let the built-in `KeyboardAwareScrollView` auto-scroll the focused input
53
- * into view. Set `false` when the screen drives its own keyboard animation
54
- * (e.g. a translateY/height worklet) so the two don't fight. @default true
52
+ * @deprecated No longer used kept for API compatibility. Keyboard handling
53
+ * now uses React Native's KeyboardAvoidingView (Expo Go compatible).
55
54
  */
56
55
  keyboardAware?: boolean;
57
56
  showHeader?: boolean;
@@ -91,7 +90,6 @@ const ScreenContainer = (props: PropsWithChildren<Props>) => {
91
90
  background = resolved === 'dark' ? ImgScreenBgGradian : undefined,
92
91
  isForm = true,
93
92
  bottomOffset = Spacing.spacing_2xl, // 32
94
- keyboardAware = true,
95
93
  showHeader = true,
96
94
  onScroll,
97
95
  onContentSizeChange,
@@ -165,28 +163,26 @@ const ScreenContainer = (props: PropsWithChildren<Props>) => {
165
163
  {renderHeader()}
166
164
 
167
165
  {isForm ? (
168
- <KeyboardAwareScrollView
169
- ScrollViewComponent={ScrollView as any}
170
- showsVerticalScrollIndicator={false}
171
- onScroll={onScroll}
172
- scrollEventThrottle={16}
173
- onContentSizeChange={onContentSizeChange}
174
- // Animate the whole form up past the keyboard (UI-thread,
175
- // Reanimated) and keep `bottomOffset` of breathing room
176
- // above the focused caret. `mode="layout"` makes the flex
177
- // layout (topSpacer / marginTop:auto actions / gap) reflow
178
- // around the keyboard space so inputs are never covered.
179
- bottomOffset={bottomOffset}
180
- mode="layout"
181
- // When false, the screen drives its own keyboard animation
182
- // (translateY/height worklet) and this view stops auto-scrolling.
183
- enabled={keyboardAware}
184
- keyboardDismissMode="interactive"
185
- keyboardShouldPersistTaps="handled"
186
- contentContainerStyle={[styles.content, style]}
166
+ // KeyboardAvoidingView (RN core) lifts the form above the
167
+ // keyboard — works in Expo Go without a custom dev build.
168
+ // `bottomOffset` keeps breathing room above the focused input.
169
+ <KeyboardAvoidingView
170
+ style={styles.flex}
171
+ behavior={isIOS ? 'padding' : undefined}
172
+ keyboardVerticalOffset={bottomOffset}
187
173
  >
188
- {body}
189
- </KeyboardAwareScrollView>
174
+ <ScrollView
175
+ showsVerticalScrollIndicator={false}
176
+ onScroll={onScroll}
177
+ scrollEventThrottle={16}
178
+ onContentSizeChange={onContentSizeChange}
179
+ keyboardDismissMode="interactive"
180
+ keyboardShouldPersistTaps="handled"
181
+ contentContainerStyle={[styles.content, style]}
182
+ >
183
+ {body}
184
+ </ScrollView>
185
+ </KeyboardAvoidingView>
190
186
  ) : (
191
187
  // Non-form screens (e.g. the auth screens) render a plain View.
192
188
  <View style={[styles.content, styles.flex, style]}>{body}</View>