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
@@ -8,8 +8,12 @@ import {
8
8
  StyleSheet,
9
9
  View,
10
10
  } from 'react-native';
11
- import FastImage, { ImageStyle as FastImageStyle } from 'react-native-fast-image';
12
- import { AppImageProps } from './app-image.type';
11
+ import { Image as ExpoImage, ImageContentFit } from 'expo-image';
12
+ import { AppImageProps, AppImageResizeMode } from './app-image.type';
13
+
14
+ // Map the RN-style resizeMode prop onto expo-image's contentFit.
15
+ const toContentFit = (mode?: AppImageResizeMode): ImageContentFit =>
16
+ mode === 'contain' ? 'contain' : mode === 'stretch' ? 'fill' : mode === 'center' ? 'none' : 'cover';
13
17
 
14
18
  const ImageSkeleton: React.FC<{ style?: StyleProp<ImageStyle> }> = ({ style }) => {
15
19
  const styles = useThemedStyles(makeStyles);
@@ -59,30 +63,23 @@ const AppImage: React.FC<AppImageProps> = ({ style, source, resizeMode, containe
59
63
  const uri = source.uri ?? '';
60
64
  return (
61
65
  <View style={[styles.container, containerStyle]}>
62
- <FastImage
66
+ <ExpoImage
63
67
  // Default (mutable) path remounts on URL change so a re-uploaded avatar
64
- // reloads instead of FastImage keeping the old native view + cached
65
- // bitmap; `cache: web` revalidates a stable URL (server fix is a unique
66
- // filename per upload, LIFEMASTER-260). An `immutable` source has a
67
- // content-addressed URL, so we skip the remount and cache it immutably —
68
- // the bitmap is served instantly on remount with no reload/flash
69
- // (LIFEMASTER-364).
68
+ // reloads instead of keeping the old cached bitmap. An `immutable` source
69
+ // has a content-addressed URL, so we skip the remount and cache it on
70
+ // memory+disk the bitmap is served instantly on remount with no flash.
70
71
  key={immutable ? undefined : uri}
71
- style={style as FastImageStyle}
72
- source={{
73
- uri: uri || '',
74
- priority: FastImage.priority.normal,
75
- cache: immutable
76
- ? FastImage.cacheControl.immutable
77
- : FastImage.cacheControl.web,
78
- }}
72
+ style={style}
73
+ source={{ uri }}
74
+ priority="normal"
75
+ cachePolicy={immutable ? 'memory-disk' : 'disk'}
76
+ contentFit={toContentFit(resizeMode)}
79
77
  // For an immutable (already-cached) source, don't re-raise the shimmer on
80
78
  // load-start: the bitmap is there instantly, so a flash to loading would
81
- // be the very reload artifact we're removing (LIFEMASTER-364).
79
+ // be the very reload artifact we're avoiding.
82
80
  onLoadStart={() => !immutable && setLoading(true)}
83
81
  onLoadEnd={() => setLoading(false)}
84
82
  onError={() => setLoading(false)}
85
- resizeMode={resizeMode}
86
83
  />
87
84
  {loading && <ImageSkeleton style={style} />}
88
85
  </View>
@@ -1,19 +1,19 @@
1
1
  import { ImagePropsBase, ImageStyle, ImageURISource, StyleProp, ViewStyle } from 'react-native';
2
- import { ResizeMode } from 'react-native-fast-image';
2
+
3
+ export type AppImageResizeMode = 'cover' | 'contain' | 'stretch' | 'center';
3
4
 
4
5
  export interface AppImageProps extends ImagePropsBase {
5
6
  source: ImageURISource | number;
6
7
  style?: StyleProp<ImageStyle>;
7
8
  containerStyle?: StyleProp<ViewStyle>;
8
- resizeMode?: ResizeMode;
9
+ resizeMode?: AppImageResizeMode;
9
10
  /**
10
11
  * Treat the remote image as immutable (stable URL → content never changes), so
11
- * FastImage serves the cached bitmap instantly on every remount instead of
12
+ * expo-image serves the cached bitmap instantly on every remount instead of
12
13
  * revalidating over the network. Use for content-addressed images (e.g. player
13
14
  * covers): it stops the artwork from flashing/reloading when the same image
14
- * remounts like expanding the mini-player to full screen on Android
15
- * (LIFEMASTER-364). Leave off for images that can change behind a stable URL
16
- * (e.g. a re-uploaded avatar), which need the default revalidating `web` cache.
15
+ * remounts. Leave off for images that can change behind a stable URL (e.g. a
16
+ * re-uploaded avatar), which remount on URL change to pick up the new bitmap.
17
17
  */
18
18
  immutable?: boolean;
19
19
  }
@@ -8,7 +8,7 @@ import {
8
8
  ImageStyle,
9
9
  Text,
10
10
  } from 'react-native';
11
- import LinearGradient from 'react-native-linear-gradient';
11
+ import { LinearGradient } from 'expo-linear-gradient';
12
12
 
13
13
  interface AvatarImageProps {
14
14
  source: ImageSourcePropType;
@@ -5,7 +5,7 @@ import {
5
5
  View,
6
6
  ViewToken,
7
7
  } from 'react-native';
8
- import FastImage from 'react-native-fast-image';
8
+ import { Image as ExpoImage } from 'expo-image';
9
9
 
10
10
  import { AppImage } from '../app-image';
11
11
  import { Label } from '../label';
@@ -35,8 +35,8 @@ const ImageSlider: React.FC<ImageSliderProps> = ({
35
35
 
36
36
  useEffect(() => {
37
37
  if (!preloadRest) return;
38
- const rest = data.slice(1).filter(Boolean).map(uri => ({ uri }));
39
- if (rest.length > 0) FastImage.preload(rest);
38
+ const rest = data.slice(1).filter(Boolean);
39
+ if (rest.length > 0) ExpoImage.prefetch(rest);
40
40
  }, [data, preloadRest]);
41
41
 
42
42
  if (data.length === 0) return null;
@@ -1,6 +1,6 @@
1
1
  import React from 'react';
2
2
  import { StyleSheet } from 'react-native';
3
- import LinearGradient from 'react-native-linear-gradient';
3
+ import { LinearGradient } from 'expo-linear-gradient';
4
4
 
5
5
  /**
6
6
  * Full-screen vertical gradient background — the default DS screen background.
@@ -1,6 +1,6 @@
1
1
  import React, { useEffect, useState } from 'react';
2
2
  import { LayoutChangeEvent, StyleSheet, View } from 'react-native';
3
- import LinearGradient from 'react-native-linear-gradient';
3
+ import { LinearGradient } from 'expo-linear-gradient';
4
4
  import Animated, {
5
5
  Easing,
6
6
  useAnimatedStyle,
@@ -2,7 +2,6 @@ import { useAuthStore, useLoadingStore } from '@src/app/stores';
2
2
  import axios, { AxiosRequestConfig, AxiosResponse } from 'axios';
3
3
  import qs from 'qs';
4
4
  import { Alert } from 'react-native';
5
- import Config from 'react-native-config';
6
5
  import { Screen } from '@src/app/navigation/app-route-type';
7
6
  import { NavigationUtil, navigationRef } from '@src/core/utils/navigation.util';
8
7
  import { Translator } from '@src/core/utils/translator.util';
@@ -15,9 +14,10 @@ import { triggerSessionEnd } from './session-end.bridge';
15
14
  // switch drives it directly.
16
15
  const acceptLanguage = (): string => (Translator.currentLanguage().startsWith('fr') ? 'fr' : 'en');
17
16
 
18
- // API Configuration
17
+ // API Configuration. Expo inlines EXPO_PUBLIC_* env vars at build time (set
18
+ // EXPO_PUBLIC_BASE_URL in .env — see .env.example).
19
19
  export const API_VERSION = 'v1';
20
- export const BaseURL = `${Config.BASE_URL}${API_VERSION}/`;
20
+ export const BaseURL = `${process.env.EXPO_PUBLIC_BASE_URL ?? ''}${API_VERSION}/`;
21
21
  // Create axios instance with default configuration
22
22
  const axiosInstance = axios.create({
23
23
  timeout: 10000,
@@ -1,10 +1,24 @@
1
1
  import AsyncStorage from '@react-native-async-storage/async-storage';
2
- import DeviceInfo from 'react-native-device-info';
2
+ import * as Application from 'expo-application';
3
+ import * as Crypto from 'expo-crypto';
4
+ import { Platform } from 'react-native';
3
5
 
4
6
  const DEVICE_ID_KEY = 'device_id';
5
7
 
6
8
  let cachedDeviceId: string | null = null;
7
9
 
10
+ // A platform-stable device identifier, resolved once and persisted so it stays
11
+ // the same across launches. Android exposes a stable id directly; iOS uses the
12
+ // vendor id (can be null on first call), so we fall back to a generated UUID.
13
+ // Both expo-application and expo-crypto ship inside Expo Go — no dev build needed.
14
+ const resolveNativeId = async (): Promise<string> => {
15
+ if (Platform.OS === 'android') {
16
+ return Application.getAndroidId() ?? Crypto.randomUUID();
17
+ }
18
+ const vendorId = await Application.getIosIdForVendorAsync();
19
+ return vendorId ?? Crypto.randomUUID();
20
+ };
21
+
8
22
  export const getDeviceId = async (): Promise<string> => {
9
23
  if (cachedDeviceId) return cachedDeviceId;
10
24
 
@@ -14,7 +28,7 @@ export const getDeviceId = async (): Promise<string> => {
14
28
  return stored;
15
29
  }
16
30
 
17
- const nativeId = await DeviceInfo.getUniqueId();
31
+ const nativeId = await resolveNativeId();
18
32
  await AsyncStorage.setItem(DEVICE_ID_KEY, nativeId);
19
33
  cachedDeviceId = nativeId;
20
34
  return nativeId;
@@ -1,4 +1,4 @@
1
- import { findBestLanguageTag } from 'react-native-localize';
1
+ import { getLocales } from 'expo-localization';
2
2
 
3
3
  /** Languages the app ships translations for. */
4
4
  export type AppLanguage = 'fr' | 'en';
@@ -9,14 +9,16 @@ const SUPPORTED: AppLanguage[] = ['fr', 'en'];
9
9
  /**
10
10
  * The device's language collapsed to a supported app language: French when the
11
11
  * phone's preferred languages best-match French, English otherwise (the default
12
- * for every other locale) — LIFEMASTER-369.
12
+ * for every other locale).
13
13
  *
14
- * Uses react-native-localize rather than the JS `Intl` API: this app's Hermes is
15
- * built from source with the Intl locale data stripped, so `Intl` reports
16
- * `en-US` regardless of the device — the native module reads the real OS
17
- * preferred-languages list instead.
14
+ * Uses expo-localization (`getLocales()` returns the OS preferred-languages
15
+ * list in order) rather than the JS `Intl` API, and ships inside Expo Go so no
16
+ * dev build is needed.
18
17
  */
19
18
  export const deviceAppLanguage = (): AppLanguage => {
20
- const best = findBestLanguageTag(SUPPORTED);
21
- return best?.languageTag === 'fr' ? 'fr' : 'en';
19
+ const supported = SUPPORTED as string[];
20
+ const match = getLocales()
21
+ .map(locale => locale.languageCode)
22
+ .find(code => code != null && supported.includes(code));
23
+ return match === 'fr' ? 'fr' : 'en';
22
24
  };
@@ -1,84 +1,63 @@
1
- import { PermissionsAndroid, Platform } from 'react-native'
2
- import {
3
- CameraOptions,
4
- ImageLibraryOptions,
5
- launchCamera,
6
- launchImageLibrary,
7
- } from 'react-native-image-picker'
1
+ import * as ExpoImagePicker from 'expo-image-picker'
2
+ import { ImagePickerOptions } from 'expo-image-picker'
8
3
 
4
+ // Thin wrapper over expo-image-picker (ships in Expo Go — no dev build needed).
5
+ // expo-image-picker handles its own runtime permission prompts on both
6
+ // platforms, so there's no manual PermissionsAndroid flow here.
9
7
  export class ImagePicker {
10
- private static optionsDefault: CameraOptions | ImageLibraryOptions = {
11
- mediaType: 'photo',
8
+ private static optionsDefault: ImagePickerOptions = {
9
+ mediaTypes: ['images'],
12
10
  quality: 0.8,
13
- maxHeight: 800,
14
- maxWidth: 800,
15
11
  }
16
12
 
17
- public static async requestCameraPermission() {
18
- if (Platform.OS === 'android') {
19
- const granted = await PermissionsAndroid.request(
20
- PermissionsAndroid.PERMISSIONS.CAMERA,
21
- {
22
- title: 'Camera Permission',
23
- message: 'App needs access to your camera',
24
- buttonNeutral: 'Ask Me Later',
25
- buttonNegative: 'Cancel',
26
- buttonPositive: 'OK',
27
- },
28
- )
29
- return granted === PermissionsAndroid.RESULTS.GRANTED
30
- }
31
- return true
13
+ public static async requestCameraPermission(): Promise<boolean> {
14
+ const { granted } = await ExpoImagePicker.requestCameraPermissionsAsync()
15
+ return granted
32
16
  }
33
17
 
34
18
  public static async lauchCamera(
35
- options?: CameraOptions,
19
+ options?: ImagePickerOptions,
36
20
  ): Promise<IMediaFormData | undefined> {
37
21
  const granted = await this.requestCameraPermission()
38
22
  if (!granted) {
39
23
  return undefined
40
24
  }
41
25
 
42
- const result = await launchCamera({
26
+ const result = await ExpoImagePicker.launchCameraAsync({
43
27
  ...this.optionsDefault,
44
28
  ...options,
45
29
  })
46
- if (result?.assets?.[0]) {
47
- const media = {
48
- name:
49
- result?.assets?.[0]?.fileName ||
50
- `img-${new Date().getTime()}`,
51
- type: result?.assets?.[0]?.type || '',
52
- uri: result?.assets?.[0]?.uri || '',
53
- }
54
- return media
55
- } else {
30
+ const asset = result.assets?.[0]
31
+ if (result.canceled || !asset) {
56
32
  return undefined
57
33
  }
34
+ return {
35
+ name: asset.fileName || `img-${new Date().getTime()}`,
36
+ type: asset.mimeType || '',
37
+ uri: asset.uri,
38
+ }
58
39
  }
59
40
 
60
41
  public static async launchImagesLibrary(
61
- options?: ImageLibraryOptions,
42
+ options?: ImagePickerOptions,
62
43
  ): Promise<IMediaFormData[]> {
63
- return new Promise((resolve, reject) => {
64
- launchImageLibrary({
65
- ...this.optionsDefault,
66
- ...options,
67
- })
68
- .then(({ assets, didCancel, errorCode }) => {
69
- if (didCancel || errorCode) {
70
- return reject(didCancel || errorCode)
71
- }
72
- const media = (assets || []).map(
73
- ({ fileName, uri, type }) => ({
74
- name: fileName || `img-${new Date().getTime()}`,
75
- type: type || '',
76
- uri: uri || '',
77
- }),
78
- )
79
- assets && resolve(media)
80
- })
81
- .catch(reject)
44
+ const { granted } =
45
+ await ExpoImagePicker.requestMediaLibraryPermissionsAsync()
46
+ if (!granted) {
47
+ return []
48
+ }
49
+
50
+ const result = await ExpoImagePicker.launchImageLibraryAsync({
51
+ ...this.optionsDefault,
52
+ ...options,
82
53
  })
54
+ if (result.canceled) {
55
+ return []
56
+ }
57
+ return (result.assets || []).map(asset => ({
58
+ name: asset.fileName || `img-${new Date().getTime()}`,
59
+ type: asset.mimeType || '',
60
+ uri: asset.uri,
61
+ }))
83
62
  }
84
63
  }
@@ -1,36 +1,31 @@
1
- import { createMMKV } from 'react-native-mmkv';
1
+ import AsyncStorage from '@react-native-async-storage/async-storage';
2
2
  import {
3
3
  PersistedClient,
4
4
  Persister,
5
5
  } from '@tanstack/react-query-persist-client';
6
6
 
7
- /**
8
- * Dedicated MMKV store for the React Query cache. Kept separate from any other
9
- * MMKV usage so clearing the query cache never touches unrelated app storage.
10
- * MMKV is synchronous + native-fast, so persisting on every cache mutation is
11
- * cheap enough to keep the on-disk snapshot fresh for an offline cold start.
12
- */
13
- const queryStorage = createMMKV({ id: 'react-query-cache' });
14
-
15
7
  const CACHE_KEY = 'REACT_QUERY_OFFLINE_CACHE';
16
8
 
17
9
  /**
18
- * A {@link Persister} backed by MMKV. The persist client writes the whole
19
- * dehydrated query cache under a single key; on launch it reads it straight
20
- * back so the last-seen feeds/lists/profile are on screen before the network
21
- * even resolves — the foundation of the offline-first experience.
10
+ * A {@link Persister} backed by AsyncStorage. The persist client writes the
11
+ * whole dehydrated query cache under a single key; on launch it reads it
12
+ * straight back so the last-seen feeds/lists/profile are on screen before the
13
+ * network even resolves — the foundation of the offline-first experience.
14
+ *
15
+ * AsyncStorage ships inside Expo Go (no dev build needed). It's async rather
16
+ * than synchronous, which the persist client fully supports.
22
17
  */
23
- export const mmkvQueryPersister: Persister = {
24
- persistClient: (client: PersistedClient) => {
25
- queryStorage.set(CACHE_KEY, JSON.stringify(client));
18
+ export const queryPersister: Persister = {
19
+ persistClient: async (client: PersistedClient) => {
20
+ await AsyncStorage.setItem(CACHE_KEY, JSON.stringify(client));
26
21
  },
27
- restoreClient: () => {
28
- const cached = queryStorage.getString(CACHE_KEY);
22
+ restoreClient: async () => {
23
+ const cached = await AsyncStorage.getItem(CACHE_KEY);
29
24
  return cached ? (JSON.parse(cached) as PersistedClient) : undefined;
30
25
  },
31
- removeClient: () => {
32
- queryStorage.remove(CACHE_KEY);
26
+ removeClient: async () => {
27
+ await AsyncStorage.removeItem(CACHE_KEY);
33
28
  },
34
29
  };
35
30
 
36
- export default mmkvQueryPersister;
31
+ export default queryPersister;
@@ -1,33 +1,110 @@
1
1
  import React from 'react';
2
- import { ActivityIndicator, StyleSheet, Text, View } from 'react-native';
2
+ import { ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
3
3
  import { useTranslation } from 'react-i18next';
4
- // Example feature API (React Query) — demonstrates the ApiService + @repo/shared
5
- // ApiEndpoints pattern. Copy core/api/example.api.ts per feature.
6
- import { useExampleList } from '@src/core/api';
7
4
  import { useTheme } from '@src/core/theme';
8
5
 
6
+ // What's inside the starter — rendered as the landing screen so a fresh
7
+ // generate is self-documenting. Replace this with your real home feature.
8
+ const FEATURES = [
9
+ { key: 'nav', icon: '🧭' },
10
+ { key: 'data', icon: '🔌' },
11
+ { key: 'i18n', icon: '🌐' },
12
+ { key: 'theme', icon: '🎨' },
13
+ { key: 'forms', icon: '📝' },
14
+ { key: 'ui', icon: '🧩' },
15
+ { key: 'state', icon: '📦' },
16
+ { key: 'shared', icon: '🔗' },
17
+ ] as const;
18
+
19
+ // Languages the starter ships translations for (see src/assets/i18n).
20
+ const LANGUAGES = [
21
+ { code: 'en', label: 'EN' },
22
+ { code: 'fr', label: 'FR' },
23
+ ] as const;
24
+
9
25
  export function HomeScreen() {
10
- const { t } = useTranslation();
26
+ const { t, i18n } = useTranslation();
11
27
  const { colors } = useTheme();
12
- const { data, isLoading, isError } = useExampleList();
28
+ const activeLng = i18n.language?.split('-')[0];
13
29
 
14
30
  return (
15
- <View style={[styles.container, { backgroundColor: colors.bg_elevation_level_1_normal }]}>
16
- <Text style={[styles.title, { color: colors.fg_neutral_normal }]}>
17
- {t('home.greeting', { name: '{{PROJECT_NAME}}' })}
18
- </Text>
19
- {isLoading ? <ActivityIndicator color={colors.fg_primary_normal} /> : null}
20
- {isError ? (
21
- <Text style={{ color: colors.bd_danger_normal }}>{t('home.loadError')}</Text>
22
- ) : null}
23
- {data ? (
24
- <Text style={{ color: colors.fg_neutral_faded }}>{t('home.count', { n: data.length })}</Text>
25
- ) : null}
26
- </View>
31
+ <ScrollView
32
+ style={{ backgroundColor: colors.bg_elevation_level_1_normal }}
33
+ contentContainerStyle={styles.container}>
34
+ <Text style={[styles.title, { color: colors.fg_neutral_normal }]}>{'{{PROJECT_NAME}}'}</Text>
35
+ <Text style={[styles.subtitle, { color: colors.fg_neutral_faded }]}>{t('home.subtitle')}</Text>
36
+
37
+ {/* Language switcher — i18n.changeLanguage re-renders every translated screen. */}
38
+ <View style={styles.langRow}>
39
+ <Text style={[styles.langLabel, { color: colors.fg_neutral_faded }]}>{t('home.language')}</Text>
40
+ {LANGUAGES.map(lng => {
41
+ const active = activeLng === lng.code;
42
+ return (
43
+ <TouchableOpacity
44
+ key={lng.code}
45
+ onPress={() => i18n.changeLanguage(lng.code)}
46
+ style={[
47
+ styles.langPill,
48
+ {
49
+ backgroundColor: active
50
+ ? colors.fg_neutral_normal
51
+ : colors.bg_elevation_level_2_normal,
52
+ borderColor: colors.bd_neutral_faded,
53
+ },
54
+ ]}>
55
+ <Text
56
+ style={[
57
+ styles.langText,
58
+ { color: active ? colors.bg_elevation_level_1_normal : colors.fg_neutral_faded },
59
+ ]}>
60
+ {lng.label}
61
+ </Text>
62
+ </TouchableOpacity>
63
+ );
64
+ })}
65
+ </View>
66
+
67
+ <View style={styles.list}>
68
+ {FEATURES.map(f => (
69
+ <View
70
+ key={f.key}
71
+ style={[
72
+ styles.card,
73
+ { backgroundColor: colors.bg_elevation_level_2_normal, borderColor: colors.bd_neutral_faded },
74
+ ]}>
75
+ <Text style={styles.icon}>{f.icon}</Text>
76
+ <View style={styles.cardText}>
77
+ <Text style={[styles.cardTitle, { color: colors.fg_neutral_normal }]}>
78
+ {t(`home.features.${f.key}.title`)}
79
+ </Text>
80
+ <Text style={[styles.cardDesc, { color: colors.fg_neutral_faded }]}>
81
+ {t(`home.features.${f.key}.desc`)}
82
+ </Text>
83
+ </View>
84
+ </View>
85
+ ))}
86
+ </View>
87
+
88
+ <Text style={[styles.footer, { color: colors.fg_neutral_faded }]}>{t('home.footer')}</Text>
89
+ <Text style={[styles.author, { color: colors.fg_neutral_faded }]}>{t('home.author')}</Text>
90
+ </ScrollView>
27
91
  );
28
92
  }
29
93
 
30
94
  const styles = StyleSheet.create({
31
- container: { flex: 1, padding: 24, gap: 16 },
32
- title: { fontSize: 22, fontWeight: '600' },
95
+ container: { padding: 20, paddingBottom: 40, gap: 8 },
96
+ title: { fontSize: 28, fontWeight: '700' },
97
+ subtitle: { fontSize: 15, marginBottom: 16, lineHeight: 21 },
98
+ langRow: { flexDirection: 'row', alignItems: 'center', gap: 8, marginBottom: 16 },
99
+ langLabel: { fontSize: 13, marginRight: 4 },
100
+ langPill: { paddingHorizontal: 14, paddingVertical: 6, borderRadius: 999, borderWidth: 1 },
101
+ langText: { fontSize: 13, fontWeight: '600' },
102
+ list: { gap: 12 },
103
+ card: { flexDirection: 'row', gap: 14, padding: 16, borderRadius: 16, borderWidth: 1 },
104
+ icon: { fontSize: 24 },
105
+ cardText: { flex: 1, gap: 4 },
106
+ cardTitle: { fontSize: 16, fontWeight: '600' },
107
+ cardDesc: { fontSize: 13, lineHeight: 19 },
108
+ footer: { fontSize: 13, marginTop: 20, textAlign: 'center', fontStyle: 'italic' },
109
+ author: { fontSize: 12, marginTop: 6, textAlign: 'center', fontWeight: '600' },
33
110
  });
@@ -0,0 +1,2 @@
1
+ BUNDLE_PATH: "vendor/bundle"
2
+ BUNDLE_FORCE_RUBY_PLATFORM: 1
@@ -0,0 +1 @@
1
+ {}
@@ -0,0 +1,17 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # You may use http://rbenv.org/ or https://rvm.io/ to install and use this version
4
+ ruby ">= 2.6.10"
5
+
6
+ # Exclude problematic versions of cocoapods and activesupport that causes build failures.
7
+ gem 'cocoapods', '>= 1.13', '!= 1.15.0', '!= 1.15.1'
8
+ gem 'activesupport', '>= 6.1.7.5', '!= 7.1.0'
9
+ gem 'xcodeproj', '< 1.26.0'
10
+ gem 'concurrent-ruby', '< 1.3.4'
11
+
12
+ # Ruby 3.4.0 has removed some libraries from the standard library.
13
+ gem 'bigdecimal'
14
+ gem 'logger'
15
+ gem 'benchmark'
16
+ gem 'mutex_m'
17
+ gem 'nkf'
@@ -40,5 +40,37 @@ pnpm --filter {{PROJECT_NAME}}-mobile start # metro
40
40
 
41
41
  Add an icon: drop `name.svg` in `src/assets/svgs/` then `pnpm --filter {{PROJECT_NAME}}-mobile sync-svgs`.
42
42
 
43
- This template ships **without** `ios/`/`android/` generate native projects (RN CLI
44
- `init` conventions) or copy yours in, then `cd ios && pod install`.
43
+ ## Native folders (ios / android)
44
+
45
+ Native projects **ship with this template** (RN 0.85.3), so no `prebuild` /
46
+ scaffolding step is needed:
47
+
48
+ ```bash
49
+ pnpm bootstrap # install
50
+ cd apps/mobile/ios && bundle install && bundle exec pod install && cd -
51
+ pnpm --filter {{PROJECT_NAME}}-mobile ios # or android
52
+ ```
53
+
54
+ The native app is named **`DuMobile`** with bundle id **`com.dumobile`** (matches
55
+ `app.json` `name`, `AppDelegate` moduleName, and `MainActivity`). **Rename these
56
+ before shipping** (Xcode target/scheme, `app.json` `name`, `android` `applicationId`
57
+ + `namespace` + the `com/dumobile` Java dir, and `index.js` registration must all match).
58
+
59
+ Monorepo note: under pnpm's hoisted layout the RN packages live in the repo-root
60
+ `node_modules`, not `apps/mobile/node_modules`. So `android/settings.gradle` and
61
+ `android/app/build.gradle` resolve `react-native`, `@react-native/gradle-plugin`
62
+ and `@react-native/codegen` via `node --print require.resolve(...)` (which walks up
63
+ to the root), and the iOS `Podfile` resolves react-native the same way. These
64
+ patches are already applied in the template.
65
+
66
+ ## Native library config (already wired)
67
+
68
+ | Library | Status |
69
+ |---------|--------|
70
+ | gesture-handler | imported first in `index.js` ✅ |
71
+ | reanimated | babel plugin set ✅ |
72
+ | svg | metro transformer set ✅ |
73
+ | keyboard-controller / toast | provider + `<Toast>` mounted ✅ |
74
+ | `react-native-config` (.env) | autolinked; if Android doesn't read `.env`, add `apply from: ".../react-native-config/android/dotenv.gradle"` to `android/app/build.gradle` |
75
+ | `react-native-image-picker` | add `NSPhotoLibraryUsageDescription` + `NSCameraUsageDescription` to `ios/DuMobile/Info.plist` and `CAMERA` / `READ_MEDIA_IMAGES` to `AndroidManifest.xml` |
76
+ | `react-native-mmkv` v4 | needs the New Architecture (default on in RN 0.85) |
@@ -3,6 +3,7 @@
3
3
  "version": "0.1.0",
4
4
  "private": true,
5
5
  "scripts": {
6
+ "dev": "react-native start",
6
7
  "start": "react-native start",
7
8
  "android": "react-native run-android",
8
9
  "ios": "react-native run-ios",
@@ -58,6 +59,7 @@
58
59
  "@react-native-community/cli-platform-ios": "20.1.3",
59
60
  "@react-native/babel-preset": "0.85.3",
60
61
  "@react-native/eslint-config": "0.85.3",
62
+ "@react-native/gradle-plugin": "0.85.3",
61
63
  "@react-native/metro-config": "0.85.3",
62
64
  "@react-native/typescript-config": "0.85.3",
63
65
  "@types/lodash": "^4.14.176",