create-du-app 0.1.2 → 0.1.4
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/README.md +56 -36
- package/package.json +6 -5
- package/src/generate.js +246 -15
- package/src/index.js +8 -8
- package/src/prompts.js +1 -1
- package/src/registry.js +3 -1
- package/templates/mobile/expo/.env.example +5 -0
- package/templates/mobile/expo/.eslintrc.js +7 -0
- package/templates/mobile/expo/.prettierrc.js +7 -0
- package/templates/mobile/expo/.svgrrc.js +9 -0
- package/templates/mobile/expo/README.md +42 -7
- package/templates/mobile/expo/_gitignore +20 -0
- package/templates/mobile/expo/_package.json +62 -1
- package/templates/mobile/expo/app.json +18 -0
- package/templates/mobile/expo/babel.config.js +21 -0
- package/templates/mobile/expo/index.js +5 -0
- package/templates/mobile/expo/metro.config.js +31 -0
- package/templates/mobile/expo/src/app/App.tsx +24 -0
- package/templates/mobile/expo/src/app/app-provider.tsx +36 -0
- package/templates/mobile/expo/src/app/config/translation.ts +26 -0
- package/templates/mobile/expo/src/app/index.ts +1 -0
- package/templates/mobile/expo/src/app/navigation/app-route-type.ts +27 -0
- package/templates/mobile/expo/src/app/navigation/bottom-tabs.tsx +34 -0
- package/templates/mobile/expo/src/app/navigation/index.tsx +14 -0
- package/templates/mobile/expo/src/app/stores/auth.store.ts +33 -0
- package/templates/mobile/expo/src/app/stores/common.store.ts +19 -0
- package/templates/mobile/expo/src/app/stores/index.ts +3 -0
- package/templates/mobile/expo/src/app/stores/loading.store.ts +22 -0
- package/templates/mobile/expo/src/assets/Images/empty-list.png +0 -0
- package/templates/mobile/expo/src/assets/Images/index.ts +5 -0
- package/templates/mobile/expo/src/assets/Images/screen-bg-gradian.png +0 -0
- package/templates/mobile/expo/src/assets/i18n/en.json +12 -0
- package/templates/mobile/expo/src/assets/i18n/fr.json +12 -0
- package/templates/mobile/expo/src/assets/lotties/index.ts +3 -0
- package/templates/mobile/expo/src/assets/lotties/loading.json +1 -0
- package/templates/mobile/expo/src/assets/svgs/arrow-left.svg +3 -0
- package/templates/mobile/expo/src/assets/svgs/arrow-right.svg +3 -0
- package/templates/mobile/expo/src/assets/svgs/calendar.svg +12 -0
- package/templates/mobile/expo/src/assets/svgs/check.svg +3 -0
- package/templates/mobile/expo/src/assets/svgs/close.svg +3 -0
- package/templates/mobile/expo/src/assets/svgs/eye-hide.svg +3 -0
- package/templates/mobile/expo/src/assets/svgs/eye.svg +4 -0
- package/templates/mobile/expo/src/assets/svgs/index.ts +29 -0
- package/templates/mobile/expo/src/assets/svgs/minus.svg +3 -0
- package/templates/mobile/expo/src/assets/svgs/plus.svg +3 -0
- package/templates/mobile/expo/src/assets/svgs/search.svg +3 -0
- package/templates/mobile/expo/src/assets/svgs/toast-error.svg +5 -0
- package/templates/mobile/expo/src/assets/svgs/toast-success.svg +4 -0
- package/templates/mobile/expo/src/core/api/endpoints.ts +8 -0
- package/templates/mobile/expo/src/core/api/example.api.ts +53 -0
- package/templates/mobile/expo/src/core/api/index.ts +4 -0
- package/templates/mobile/expo/src/core/components/common/index.ts +1 -0
- package/templates/mobile/expo/src/core/components/common/list-empty/index.ts +2 -0
- package/templates/mobile/expo/src/core/components/common/list-empty/list-empty.tsx +30 -0
- package/templates/mobile/expo/src/core/components/common/list-empty/list-empty.type.ts +5 -0
- package/templates/mobile/expo/src/core/components/forms/hf-date-time.tsx +301 -0
- package/templates/mobile/expo/src/core/components/forms/hf-password-input.tsx +153 -0
- package/templates/mobile/expo/src/core/components/forms/hf-text-input.tsx +59 -0
- package/templates/mobile/expo/src/core/components/forms/hf-time-picker.tsx +117 -0
- package/templates/mobile/expo/src/core/components/forms/index.ts +4 -0
- package/templates/mobile/expo/src/core/components/index.ts +5 -0
- package/templates/mobile/expo/src/core/components/loading/index.ts +1 -0
- package/templates/mobile/expo/src/core/components/loading/loading-app/index.ts +1 -0
- package/templates/mobile/expo/src/core/components/loading/loading-app/loading-app.tsx +50 -0
- package/templates/mobile/expo/src/core/components/offline/index.ts +1 -0
- package/templates/mobile/expo/src/core/components/offline/offline-banner.tsx +186 -0
- package/templates/mobile/expo/src/core/components/screen/index.ts +1 -0
- package/templates/mobile/expo/src/core/components/screen/screen-container/index.ts +1 -0
- package/templates/mobile/expo/src/core/components/screen/screen-container/screen-container.tsx +252 -0
- package/templates/mobile/expo/src/core/components/splash/index.ts +1 -0
- package/templates/mobile/expo/src/core/components/splash/splash-overlay/index.ts +1 -0
- package/templates/mobile/expo/src/core/components/splash/splash-overlay/splash-overlay.tsx +93 -0
- package/templates/mobile/expo/src/core/components/ui/animated-list-item/animated-list-item.tsx +48 -0
- package/templates/mobile/expo/src/core/components/ui/animated-list-item/animated-list-item.type.ts +10 -0
- package/templates/mobile/expo/src/core/components/ui/animated-list-item/index.ts +2 -0
- package/templates/mobile/expo/src/core/components/ui/app-image/app-image.tsx +104 -0
- package/templates/mobile/expo/src/core/components/ui/app-image/app-image.type.ts +19 -0
- package/templates/mobile/expo/src/core/components/ui/app-image/index.ts +2 -0
- package/templates/mobile/expo/src/core/components/ui/asset-placeholder/asset-placeholder.tsx +76 -0
- package/templates/mobile/expo/src/core/components/ui/asset-placeholder/index.ts +1 -0
- package/templates/mobile/expo/src/core/components/ui/avatar-image/avatar-image.tsx +90 -0
- package/templates/mobile/expo/src/core/components/ui/avatar-image/index.ts +1 -0
- package/templates/mobile/expo/src/core/components/ui/bottom-sheet/bottom-sheet.tsx +145 -0
- package/templates/mobile/expo/src/core/components/ui/bottom-sheet/bottom-sheet.type.ts +10 -0
- package/templates/mobile/expo/src/core/components/ui/bottom-sheet/index.ts +2 -0
- package/templates/mobile/expo/src/core/components/ui/button/button.style.ts +146 -0
- package/templates/mobile/expo/src/core/components/ui/button/button.tsx +97 -0
- package/templates/mobile/expo/src/core/components/ui/button/button.type.ts +47 -0
- package/templates/mobile/expo/src/core/components/ui/button/index.ts +4 -0
- package/templates/mobile/expo/src/core/components/ui/checkbox/checkbox.tsx +127 -0
- package/templates/mobile/expo/src/core/components/ui/checkbox/checkbox.type.ts +24 -0
- package/templates/mobile/expo/src/core/components/ui/checkbox/index.ts +2 -0
- package/templates/mobile/expo/src/core/components/ui/collapsible-section/collapsible-section.tsx +141 -0
- package/templates/mobile/expo/src/core/components/ui/collapsible-section/collapsible-section.type.ts +10 -0
- package/templates/mobile/expo/src/core/components/ui/collapsible-section/index.ts +2 -0
- package/templates/mobile/expo/src/core/components/ui/components.registry.json +313 -0
- package/templates/mobile/expo/src/core/components/ui/field/field-frame.tsx +62 -0
- package/templates/mobile/expo/src/core/components/ui/field/field.shared.ts +31 -0
- package/templates/mobile/expo/src/core/components/ui/header/header.tsx +196 -0
- package/templates/mobile/expo/src/core/components/ui/header/header.type.ts +30 -0
- package/templates/mobile/expo/src/core/components/ui/header/index.ts +2 -0
- package/templates/mobile/expo/src/core/components/ui/icon-button/icon-button.style.ts +23 -0
- package/templates/mobile/expo/src/core/components/ui/icon-button/icon-button.tsx +66 -0
- package/templates/mobile/expo/src/core/components/ui/icon-button/icon-button.type.ts +25 -0
- package/templates/mobile/expo/src/core/components/ui/icon-button/index.ts +2 -0
- package/templates/mobile/expo/src/core/components/ui/image-slider/image-slider.tsx +107 -0
- package/templates/mobile/expo/src/core/components/ui/image-slider/image-slider.type.ts +10 -0
- package/templates/mobile/expo/src/core/components/ui/image-slider/index.ts +2 -0
- package/templates/mobile/expo/src/core/components/ui/index.ts +25 -0
- package/templates/mobile/expo/src/core/components/ui/label/index.ts +2 -0
- package/templates/mobile/expo/src/core/components/ui/label/label.tsx +36 -0
- package/templates/mobile/expo/src/core/components/ui/label/label.type.ts +12 -0
- package/templates/mobile/expo/src/core/components/ui/modal/index.ts +2 -0
- package/templates/mobile/expo/src/core/components/ui/modal/modal.tsx +62 -0
- package/templates/mobile/expo/src/core/components/ui/modal/modal.type.ts +11 -0
- package/templates/mobile/expo/src/core/components/ui/otp-input/index.ts +2 -0
- package/templates/mobile/expo/src/core/components/ui/otp-input/otp-input.tsx +129 -0
- package/templates/mobile/expo/src/core/components/ui/otp-input/otp-input.type.ts +20 -0
- package/templates/mobile/expo/src/core/components/ui/page-dots/index.ts +1 -0
- package/templates/mobile/expo/src/core/components/ui/page-dots/page-dots.tsx +60 -0
- package/templates/mobile/expo/src/core/components/ui/radio/index.ts +2 -0
- package/templates/mobile/expo/src/core/components/ui/radio/radio.tsx +121 -0
- package/templates/mobile/expo/src/core/components/ui/radio/radio.type.ts +20 -0
- package/templates/mobile/expo/src/core/components/ui/screen/index.ts +1 -0
- package/templates/mobile/expo/src/core/components/ui/screen/screen-gradient.tsx +33 -0
- package/templates/mobile/expo/src/core/components/ui/search-box/index.ts +2 -0
- package/templates/mobile/expo/src/core/components/ui/search-box/search-box.tsx +162 -0
- package/templates/mobile/expo/src/core/components/ui/search-box/search-box.type.ts +26 -0
- package/templates/mobile/expo/src/core/components/ui/segmented-control/index.ts +2 -0
- package/templates/mobile/expo/src/core/components/ui/segmented-control/segmented-control.tsx +86 -0
- package/templates/mobile/expo/src/core/components/ui/segmented-control/segmented-control.type.ts +22 -0
- package/templates/mobile/expo/src/core/components/ui/skeleton/index.ts +2 -0
- package/templates/mobile/expo/src/core/components/ui/skeleton/skeleton.tsx +106 -0
- package/templates/mobile/expo/src/core/components/ui/skeleton/skeleton.type.ts +8 -0
- package/templates/mobile/expo/src/core/components/ui/success-state/index.ts +1 -0
- package/templates/mobile/expo/src/core/components/ui/success-state/success-state.tsx +68 -0
- package/templates/mobile/expo/src/core/components/ui/tabs/index.ts +2 -0
- package/templates/mobile/expo/src/core/components/ui/tabs/tabs.tsx +273 -0
- package/templates/mobile/expo/src/core/components/ui/tabs/tabs.type.ts +21 -0
- package/templates/mobile/expo/src/core/components/ui/tag-input/index.ts +2 -0
- package/templates/mobile/expo/src/core/components/ui/tag-input/tag-input.tsx +146 -0
- package/templates/mobile/expo/src/core/components/ui/tag-input/tag-input.type.ts +22 -0
- package/templates/mobile/expo/src/core/components/ui/text-area/index.ts +2 -0
- package/templates/mobile/expo/src/core/components/ui/text-area/text-area.tsx +90 -0
- package/templates/mobile/expo/src/core/components/ui/text-area/text-area.type.ts +20 -0
- package/templates/mobile/expo/src/core/components/ui/text-field/index.ts +2 -0
- package/templates/mobile/expo/src/core/components/ui/text-field/text-field.tsx +116 -0
- package/templates/mobile/expo/src/core/components/ui/text-field/text-field.type.ts +21 -0
- package/templates/mobile/expo/src/core/components/ui/toggle/index.ts +2 -0
- package/templates/mobile/expo/src/core/components/ui/toggle/toggle.tsx +110 -0
- package/templates/mobile/expo/src/core/components/ui/toggle/toggle.type.ts +19 -0
- package/templates/mobile/expo/src/core/constants/external-urls.constant.ts +5 -0
- package/templates/mobile/expo/src/core/constants/hard-data.constant.ts +0 -0
- package/templates/mobile/expo/src/core/constants/index.ts +2 -0
- package/templates/mobile/expo/src/core/constants/type.constant.ts +3 -0
- package/templates/mobile/expo/src/core/context/index.ts +1 -0
- package/templates/mobile/expo/src/core/context/shared-transition-context.tsx +35 -0
- package/templates/mobile/expo/src/core/hook/index.ts +8 -0
- package/templates/mobile/expo/src/core/hook/useActiveRouteName.ts +63 -0
- package/templates/mobile/expo/src/core/hook/useAppNavigation.tsx +7 -0
- package/templates/mobile/expo/src/core/hook/useBottomInset.tsx +26 -0
- package/templates/mobile/expo/src/core/hook/useDebounce.tsx +16 -0
- package/templates/mobile/expo/src/core/hook/useEndReached.tsx +46 -0
- package/templates/mobile/expo/src/core/hook/useManualRefetch.ts +56 -0
- package/templates/mobile/expo/src/core/hook/useNetworkStatus.ts +68 -0
- package/templates/mobile/expo/src/core/hook/useTimeout.tsx +30 -0
- package/templates/mobile/expo/src/core/index.ts +7 -0
- package/templates/mobile/expo/src/core/services/api.service.ts +230 -0
- package/templates/mobile/expo/src/core/services/device-id.service.ts +23 -0
- package/templates/mobile/expo/src/core/services/index.ts +3 -0
- package/templates/mobile/expo/src/core/services/session-end.bridge.ts +26 -0
- package/templates/mobile/expo/src/core/theme/dark.theme.ts +10 -0
- package/templates/mobile/expo/src/core/theme/index.ts +5 -0
- package/templates/mobile/expo/src/core/theme/light.theme.ts +44 -0
- package/templates/mobile/expo/src/core/theme/theme-context.tsx +82 -0
- package/templates/mobile/expo/src/core/theme/theme.types.ts +26 -0
- package/templates/mobile/expo/src/core/theme/use-themed-styles.ts +25 -0
- package/templates/mobile/expo/src/core/utils/color.util.tsx +198 -0
- package/templates/mobile/expo/src/core/utils/date.util.ts +97 -0
- package/templates/mobile/expo/src/core/utils/device-locale.util.ts +22 -0
- package/templates/mobile/expo/src/core/utils/emitter/index.ts +161 -0
- package/templates/mobile/expo/src/core/utils/emitter/type.ts +40 -0
- package/templates/mobile/expo/src/core/utils/enum.util.tsx +15 -0
- package/templates/mobile/expo/src/core/utils/font.util.tsx +42 -0
- package/templates/mobile/expo/src/core/utils/func.util.ts +48 -0
- package/templates/mobile/expo/src/core/utils/greeting.util.ts +20 -0
- package/templates/mobile/expo/src/core/utils/image-format.util.ts +117 -0
- package/templates/mobile/expo/src/core/utils/image-picker.util.ts +84 -0
- package/templates/mobile/expo/src/core/utils/index.ts +18 -0
- package/templates/mobile/expo/src/core/utils/linking.util.ts +16 -0
- package/templates/mobile/expo/src/core/utils/navigation.util.tsx +100 -0
- package/templates/mobile/expo/src/core/utils/number-format.ts +28 -0
- package/templates/mobile/expo/src/core/utils/query-client.util.ts +35 -0
- package/templates/mobile/expo/src/core/utils/query-persister.util.ts +36 -0
- package/templates/mobile/expo/src/core/utils/schema.util.tsx +2713 -0
- package/templates/mobile/expo/src/core/utils/size.util.tsx +74 -0
- package/templates/mobile/expo/src/core/utils/storage.util.tsx +53 -0
- package/templates/mobile/expo/src/core/utils/toast.util.tsx +151 -0
- package/templates/mobile/expo/src/core/utils/translator.util.tsx +23 -0
- package/templates/mobile/expo/src/core/utils/typography.util.tsx +69 -0
- package/templates/mobile/expo/src/core/utils/validate.util.tsx +13 -0
- package/templates/mobile/expo/src/declarations.d.ts +54 -0
- package/templates/mobile/expo/src/modules/home/home.screen.tsx +33 -0
- package/templates/mobile/expo/src/modules/profile/profile.screen.tsx +29 -0
- package/templates/mobile/expo/src/scripts/link-fonts.js +60 -0
- package/templates/mobile/expo/src/scripts/sync-images.js +56 -0
- package/templates/mobile/expo/src/scripts/sync-svgs.js +50 -0
- package/templates/mobile/expo/src/types/models.d.ts +24 -0
- package/templates/mobile/expo/tsconfig.json +19 -0
- package/templates/mobile/rn/.env.example +5 -0
- package/templates/mobile/rn/.eslintrc.js +7 -0
- package/templates/mobile/rn/.prettierrc.js +7 -0
- package/templates/mobile/rn/.svgrrc.js +9 -0
- package/templates/mobile/rn/README.md +40 -7
- package/templates/mobile/rn/_gitignore +24 -0
- package/templates/mobile/rn/_package.json +67 -1
- package/templates/mobile/rn/app.json +4 -0
- package/templates/mobile/rn/babel.config.js +18 -0
- package/templates/mobile/rn/index.js +8 -0
- package/templates/mobile/rn/metro.config.js +33 -0
- package/templates/mobile/rn/src/app/App.tsx +24 -0
- package/templates/mobile/rn/src/app/app-provider.tsx +36 -0
- package/templates/mobile/rn/src/app/config/translation.ts +26 -0
- package/templates/mobile/rn/src/app/index.ts +1 -0
- package/templates/mobile/rn/src/app/navigation/app-route-type.ts +27 -0
- package/templates/mobile/rn/src/app/navigation/bottom-tabs.tsx +34 -0
- package/templates/mobile/rn/src/app/navigation/index.tsx +14 -0
- package/templates/mobile/rn/src/app/stores/auth.store.ts +33 -0
- package/templates/mobile/rn/src/app/stores/common.store.ts +19 -0
- package/templates/mobile/rn/src/app/stores/index.ts +3 -0
- package/templates/mobile/rn/src/app/stores/loading.store.ts +22 -0
- package/templates/mobile/rn/src/assets/Images/empty-list.png +0 -0
- package/templates/mobile/rn/src/assets/Images/index.ts +5 -0
- package/templates/mobile/rn/src/assets/Images/screen-bg-gradian.png +0 -0
- package/templates/mobile/rn/src/assets/i18n/en.json +12 -0
- package/templates/mobile/rn/src/assets/i18n/fr.json +12 -0
- package/templates/mobile/rn/src/assets/lotties/index.ts +3 -0
- package/templates/mobile/rn/src/assets/lotties/loading.json +1 -0
- package/templates/mobile/rn/src/assets/svgs/arrow-left.svg +3 -0
- package/templates/mobile/rn/src/assets/svgs/arrow-right.svg +3 -0
- package/templates/mobile/rn/src/assets/svgs/calendar.svg +12 -0
- package/templates/mobile/rn/src/assets/svgs/check.svg +3 -0
- package/templates/mobile/rn/src/assets/svgs/close.svg +3 -0
- package/templates/mobile/rn/src/assets/svgs/eye-hide.svg +3 -0
- package/templates/mobile/rn/src/assets/svgs/eye.svg +4 -0
- package/templates/mobile/rn/src/assets/svgs/index.ts +29 -0
- package/templates/mobile/rn/src/assets/svgs/minus.svg +3 -0
- package/templates/mobile/rn/src/assets/svgs/plus.svg +3 -0
- package/templates/mobile/rn/src/assets/svgs/search.svg +3 -0
- package/templates/mobile/rn/src/assets/svgs/toast-error.svg +5 -0
- package/templates/mobile/rn/src/assets/svgs/toast-success.svg +4 -0
- package/templates/mobile/rn/src/core/api/endpoints.ts +8 -0
- package/templates/mobile/rn/src/core/api/example.api.ts +53 -0
- package/templates/mobile/rn/src/core/api/index.ts +4 -0
- package/templates/mobile/rn/src/core/components/common/index.ts +1 -0
- package/templates/mobile/rn/src/core/components/common/list-empty/index.ts +2 -0
- package/templates/mobile/rn/src/core/components/common/list-empty/list-empty.tsx +30 -0
- package/templates/mobile/rn/src/core/components/common/list-empty/list-empty.type.ts +5 -0
- package/templates/mobile/rn/src/core/components/forms/hf-date-time.tsx +301 -0
- package/templates/mobile/rn/src/core/components/forms/hf-password-input.tsx +153 -0
- package/templates/mobile/rn/src/core/components/forms/hf-text-input.tsx +59 -0
- package/templates/mobile/rn/src/core/components/forms/hf-time-picker.tsx +117 -0
- package/templates/mobile/rn/src/core/components/forms/index.ts +4 -0
- package/templates/mobile/rn/src/core/components/index.ts +5 -0
- package/templates/mobile/rn/src/core/components/loading/index.ts +1 -0
- package/templates/mobile/rn/src/core/components/loading/loading-app/index.ts +1 -0
- package/templates/mobile/rn/src/core/components/loading/loading-app/loading-app.tsx +50 -0
- package/templates/mobile/rn/src/core/components/offline/index.ts +1 -0
- package/templates/mobile/rn/src/core/components/offline/offline-banner.tsx +186 -0
- package/templates/mobile/rn/src/core/components/screen/index.ts +1 -0
- package/templates/mobile/rn/src/core/components/screen/screen-container/index.ts +1 -0
- package/templates/mobile/rn/src/core/components/screen/screen-container/screen-container.tsx +252 -0
- package/templates/mobile/rn/src/core/components/splash/index.ts +1 -0
- package/templates/mobile/rn/src/core/components/splash/splash-overlay/index.ts +1 -0
- package/templates/mobile/rn/src/core/components/splash/splash-overlay/splash-overlay.tsx +93 -0
- package/templates/mobile/rn/src/core/components/ui/animated-list-item/animated-list-item.tsx +48 -0
- package/templates/mobile/rn/src/core/components/ui/animated-list-item/animated-list-item.type.ts +10 -0
- package/templates/mobile/rn/src/core/components/ui/animated-list-item/index.ts +2 -0
- package/templates/mobile/rn/src/core/components/ui/app-image/app-image.tsx +104 -0
- package/templates/mobile/rn/src/core/components/ui/app-image/app-image.type.ts +19 -0
- package/templates/mobile/rn/src/core/components/ui/app-image/index.ts +2 -0
- package/templates/mobile/rn/src/core/components/ui/asset-placeholder/asset-placeholder.tsx +76 -0
- package/templates/mobile/rn/src/core/components/ui/asset-placeholder/index.ts +1 -0
- package/templates/mobile/rn/src/core/components/ui/avatar-image/avatar-image.tsx +90 -0
- package/templates/mobile/rn/src/core/components/ui/avatar-image/index.ts +1 -0
- package/templates/mobile/rn/src/core/components/ui/bottom-sheet/bottom-sheet.tsx +145 -0
- package/templates/mobile/rn/src/core/components/ui/bottom-sheet/bottom-sheet.type.ts +10 -0
- package/templates/mobile/rn/src/core/components/ui/bottom-sheet/index.ts +2 -0
- package/templates/mobile/rn/src/core/components/ui/button/button.style.ts +146 -0
- package/templates/mobile/rn/src/core/components/ui/button/button.tsx +97 -0
- package/templates/mobile/rn/src/core/components/ui/button/button.type.ts +47 -0
- package/templates/mobile/rn/src/core/components/ui/button/index.ts +4 -0
- package/templates/mobile/rn/src/core/components/ui/checkbox/checkbox.tsx +127 -0
- package/templates/mobile/rn/src/core/components/ui/checkbox/checkbox.type.ts +24 -0
- package/templates/mobile/rn/src/core/components/ui/checkbox/index.ts +2 -0
- package/templates/mobile/rn/src/core/components/ui/collapsible-section/collapsible-section.tsx +141 -0
- package/templates/mobile/rn/src/core/components/ui/collapsible-section/collapsible-section.type.ts +10 -0
- package/templates/mobile/rn/src/core/components/ui/collapsible-section/index.ts +2 -0
- package/templates/mobile/rn/src/core/components/ui/components.registry.json +313 -0
- package/templates/mobile/rn/src/core/components/ui/field/field-frame.tsx +62 -0
- package/templates/mobile/rn/src/core/components/ui/field/field.shared.ts +31 -0
- package/templates/mobile/rn/src/core/components/ui/header/header.tsx +196 -0
- package/templates/mobile/rn/src/core/components/ui/header/header.type.ts +30 -0
- package/templates/mobile/rn/src/core/components/ui/header/index.ts +2 -0
- package/templates/mobile/rn/src/core/components/ui/icon-button/icon-button.style.ts +23 -0
- package/templates/mobile/rn/src/core/components/ui/icon-button/icon-button.tsx +66 -0
- package/templates/mobile/rn/src/core/components/ui/icon-button/icon-button.type.ts +25 -0
- package/templates/mobile/rn/src/core/components/ui/icon-button/index.ts +2 -0
- package/templates/mobile/rn/src/core/components/ui/image-slider/image-slider.tsx +107 -0
- package/templates/mobile/rn/src/core/components/ui/image-slider/image-slider.type.ts +10 -0
- package/templates/mobile/rn/src/core/components/ui/image-slider/index.ts +2 -0
- package/templates/mobile/rn/src/core/components/ui/index.ts +25 -0
- package/templates/mobile/rn/src/core/components/ui/label/index.ts +2 -0
- package/templates/mobile/rn/src/core/components/ui/label/label.tsx +36 -0
- package/templates/mobile/rn/src/core/components/ui/label/label.type.ts +12 -0
- package/templates/mobile/rn/src/core/components/ui/modal/index.ts +2 -0
- package/templates/mobile/rn/src/core/components/ui/modal/modal.tsx +62 -0
- package/templates/mobile/rn/src/core/components/ui/modal/modal.type.ts +11 -0
- package/templates/mobile/rn/src/core/components/ui/otp-input/index.ts +2 -0
- package/templates/mobile/rn/src/core/components/ui/otp-input/otp-input.tsx +129 -0
- package/templates/mobile/rn/src/core/components/ui/otp-input/otp-input.type.ts +20 -0
- package/templates/mobile/rn/src/core/components/ui/page-dots/index.ts +1 -0
- package/templates/mobile/rn/src/core/components/ui/page-dots/page-dots.tsx +60 -0
- package/templates/mobile/rn/src/core/components/ui/radio/index.ts +2 -0
- package/templates/mobile/rn/src/core/components/ui/radio/radio.tsx +121 -0
- package/templates/mobile/rn/src/core/components/ui/radio/radio.type.ts +20 -0
- package/templates/mobile/rn/src/core/components/ui/screen/index.ts +1 -0
- package/templates/mobile/rn/src/core/components/ui/screen/screen-gradient.tsx +33 -0
- package/templates/mobile/rn/src/core/components/ui/search-box/index.ts +2 -0
- package/templates/mobile/rn/src/core/components/ui/search-box/search-box.tsx +162 -0
- package/templates/mobile/rn/src/core/components/ui/search-box/search-box.type.ts +26 -0
- package/templates/mobile/rn/src/core/components/ui/segmented-control/index.ts +2 -0
- package/templates/mobile/rn/src/core/components/ui/segmented-control/segmented-control.tsx +86 -0
- package/templates/mobile/rn/src/core/components/ui/segmented-control/segmented-control.type.ts +22 -0
- package/templates/mobile/rn/src/core/components/ui/skeleton/index.ts +2 -0
- package/templates/mobile/rn/src/core/components/ui/skeleton/skeleton.tsx +106 -0
- package/templates/mobile/rn/src/core/components/ui/skeleton/skeleton.type.ts +8 -0
- package/templates/mobile/rn/src/core/components/ui/success-state/index.ts +1 -0
- package/templates/mobile/rn/src/core/components/ui/success-state/success-state.tsx +68 -0
- package/templates/mobile/rn/src/core/components/ui/tabs/index.ts +2 -0
- package/templates/mobile/rn/src/core/components/ui/tabs/tabs.tsx +273 -0
- package/templates/mobile/rn/src/core/components/ui/tabs/tabs.type.ts +21 -0
- package/templates/mobile/rn/src/core/components/ui/tag-input/index.ts +2 -0
- package/templates/mobile/rn/src/core/components/ui/tag-input/tag-input.tsx +146 -0
- package/templates/mobile/rn/src/core/components/ui/tag-input/tag-input.type.ts +22 -0
- package/templates/mobile/rn/src/core/components/ui/text-area/index.ts +2 -0
- package/templates/mobile/rn/src/core/components/ui/text-area/text-area.tsx +90 -0
- package/templates/mobile/rn/src/core/components/ui/text-area/text-area.type.ts +20 -0
- package/templates/mobile/rn/src/core/components/ui/text-field/index.ts +2 -0
- package/templates/mobile/rn/src/core/components/ui/text-field/text-field.tsx +116 -0
- package/templates/mobile/rn/src/core/components/ui/text-field/text-field.type.ts +21 -0
- package/templates/mobile/rn/src/core/components/ui/toggle/index.ts +2 -0
- package/templates/mobile/rn/src/core/components/ui/toggle/toggle.tsx +110 -0
- package/templates/mobile/rn/src/core/components/ui/toggle/toggle.type.ts +19 -0
- package/templates/mobile/rn/src/core/constants/external-urls.constant.ts +5 -0
- package/templates/mobile/rn/src/core/constants/hard-data.constant.ts +0 -0
- package/templates/mobile/rn/src/core/constants/index.ts +2 -0
- package/templates/mobile/rn/src/core/constants/type.constant.ts +3 -0
- package/templates/mobile/rn/src/core/context/index.ts +1 -0
- package/templates/mobile/rn/src/core/context/shared-transition-context.tsx +35 -0
- package/templates/mobile/rn/src/core/hook/index.ts +8 -0
- package/templates/mobile/rn/src/core/hook/useActiveRouteName.ts +63 -0
- package/templates/mobile/rn/src/core/hook/useAppNavigation.tsx +7 -0
- package/templates/mobile/rn/src/core/hook/useBottomInset.tsx +26 -0
- package/templates/mobile/rn/src/core/hook/useDebounce.tsx +16 -0
- package/templates/mobile/rn/src/core/hook/useEndReached.tsx +46 -0
- package/templates/mobile/rn/src/core/hook/useManualRefetch.ts +56 -0
- package/templates/mobile/rn/src/core/hook/useNetworkStatus.ts +68 -0
- package/templates/mobile/rn/src/core/hook/useTimeout.tsx +30 -0
- package/templates/mobile/rn/src/core/index.ts +7 -0
- package/templates/mobile/rn/src/core/services/api.service.ts +230 -0
- package/templates/mobile/rn/src/core/services/device-id.service.ts +23 -0
- package/templates/mobile/rn/src/core/services/index.ts +3 -0
- package/templates/mobile/rn/src/core/services/session-end.bridge.ts +26 -0
- package/templates/mobile/rn/src/core/theme/dark.theme.ts +10 -0
- package/templates/mobile/rn/src/core/theme/index.ts +5 -0
- package/templates/mobile/rn/src/core/theme/light.theme.ts +44 -0
- package/templates/mobile/rn/src/core/theme/theme-context.tsx +82 -0
- package/templates/mobile/rn/src/core/theme/theme.types.ts +26 -0
- package/templates/mobile/rn/src/core/theme/use-themed-styles.ts +25 -0
- package/templates/mobile/rn/src/core/utils/color.util.tsx +198 -0
- package/templates/mobile/rn/src/core/utils/date.util.ts +97 -0
- package/templates/mobile/rn/src/core/utils/device-locale.util.ts +22 -0
- package/templates/mobile/rn/src/core/utils/emitter/index.ts +161 -0
- package/templates/mobile/rn/src/core/utils/emitter/type.ts +40 -0
- package/templates/mobile/rn/src/core/utils/enum.util.tsx +15 -0
- package/templates/mobile/rn/src/core/utils/font.util.tsx +42 -0
- package/templates/mobile/rn/src/core/utils/func.util.ts +48 -0
- package/templates/mobile/rn/src/core/utils/greeting.util.ts +20 -0
- package/templates/mobile/rn/src/core/utils/image-format.util.ts +117 -0
- package/templates/mobile/rn/src/core/utils/image-picker.util.ts +84 -0
- package/templates/mobile/rn/src/core/utils/index.ts +18 -0
- package/templates/mobile/rn/src/core/utils/linking.util.ts +16 -0
- package/templates/mobile/rn/src/core/utils/navigation.util.tsx +100 -0
- package/templates/mobile/rn/src/core/utils/number-format.ts +28 -0
- package/templates/mobile/rn/src/core/utils/query-client.util.ts +35 -0
- package/templates/mobile/rn/src/core/utils/query-persister.util.ts +36 -0
- package/templates/mobile/rn/src/core/utils/schema.util.tsx +2713 -0
- package/templates/mobile/rn/src/core/utils/size.util.tsx +74 -0
- package/templates/mobile/rn/src/core/utils/storage.util.tsx +53 -0
- package/templates/mobile/rn/src/core/utils/toast.util.tsx +151 -0
- package/templates/mobile/rn/src/core/utils/translator.util.tsx +23 -0
- package/templates/mobile/rn/src/core/utils/typography.util.tsx +69 -0
- package/templates/mobile/rn/src/core/utils/validate.util.tsx +13 -0
- package/templates/mobile/rn/src/declarations.d.ts +54 -0
- package/templates/mobile/rn/src/modules/home/home.screen.tsx +33 -0
- package/templates/mobile/rn/src/modules/profile/profile.screen.tsx +29 -0
- package/templates/mobile/rn/src/scripts/link-fonts.js +60 -0
- package/templates/mobile/rn/src/scripts/sync-images.js +56 -0
- package/templates/mobile/rn/src/scripts/sync-svgs.js +50 -0
- package/templates/mobile/rn/src/types/models.d.ts +24 -0
- package/templates/mobile/rn/tsconfig.json +21 -0
- package/templates/shared/README.md +21 -6
- package/templates/shared/_package.json +26 -4
- package/templates/shared/src/api-endpoints.ts +8 -0
- package/templates/shared/src/enums.ts +34 -0
- package/templates/shared/src/external-urls.ts +5 -0
- package/templates/shared/src/index.ts +15 -0
- package/templates/shared/tsconfig.json +8 -0
- package/templates/shared/tsup.config.ts +9 -0
- package/templates/shared/index.js +0 -4
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
import NetInfo, { NetInfoState } from '@react-native-community/netinfo';
|
|
3
|
+
|
|
4
|
+
export interface NetworkStatus {
|
|
5
|
+
/** The device has an active network interface (wifi / cellular). */
|
|
6
|
+
isConnected: boolean;
|
|
7
|
+
/**
|
|
8
|
+
* The interface can actually reach the internet — distinguishes a captive
|
|
9
|
+
* portal or a "connected but no data" wifi from a real connection. `null`
|
|
10
|
+
* while NetInfo is still probing (treated as reachable to avoid a false
|
|
11
|
+
* offline flash on launch).
|
|
12
|
+
*/
|
|
13
|
+
isInternetReachable: boolean | null;
|
|
14
|
+
/**
|
|
15
|
+
* The single flag screens should branch on: truly offline only when there
|
|
16
|
+
* is no interface, or the interface is confirmed unable to reach the
|
|
17
|
+
* internet. A `null` (probing) reachability does NOT count as offline.
|
|
18
|
+
*/
|
|
19
|
+
isOffline: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Maps a raw NetInfo state to our {@link NetworkStatus}. Exported for unit
|
|
24
|
+
* testing the offline-detection rule (connected + reachable handling).
|
|
25
|
+
*/
|
|
26
|
+
export const deriveNetworkStatus = (state: NetInfoState): NetworkStatus => {
|
|
27
|
+
const isConnected = state.isConnected ?? true;
|
|
28
|
+
const isInternetReachable = state.isInternetReachable;
|
|
29
|
+
const isOffline = !isConnected || isInternetReachable === false;
|
|
30
|
+
return { isConnected, isInternetReachable, isOffline };
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Latest known offline state, kept in sync by a module-scope NetInfo listener.
|
|
35
|
+
* Lets non-React code (the player store / queue transport, which runs outside
|
|
36
|
+
* the component tree) read offline status synchronously via {@link isDeviceOffline}
|
|
37
|
+
* — the React hook below can't reach those call sites. Starts `false` so we never
|
|
38
|
+
* report a false "offline" before NetInfo's first probe lands.
|
|
39
|
+
*/
|
|
40
|
+
let deviceOffline = false;
|
|
41
|
+
NetInfo.addEventListener(state => {
|
|
42
|
+
deviceOffline = deriveNetworkStatus(state).isOffline;
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Synchronous offline check for non-React callers (e.g. the player queue, which
|
|
47
|
+
* must decide whether stepping to the next track is playable without internet).
|
|
48
|
+
* Components should use {@link useNetworkStatus} instead so they re-render on change.
|
|
49
|
+
*/
|
|
50
|
+
export const isDeviceOffline = (): boolean => deviceOffline;
|
|
51
|
+
|
|
52
|
+
export const useNetworkStatus = (): NetworkStatus => {
|
|
53
|
+
const [status, setStatus] = useState<NetworkStatus>({
|
|
54
|
+
isConnected: true,
|
|
55
|
+
isInternetReachable: null,
|
|
56
|
+
isOffline: false,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
const unsubscribe = NetInfo.addEventListener(state => {
|
|
61
|
+
setStatus(deriveNetworkStatus(state));
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
return unsubscribe;
|
|
65
|
+
}, []);
|
|
66
|
+
|
|
67
|
+
return status;
|
|
68
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef } from 'react';
|
|
2
|
+
|
|
3
|
+
export default function useTimeout(callback: () => void, delay: number) {
|
|
4
|
+
const callbackRef = useRef(callback);
|
|
5
|
+
const timeoutRef = useRef<any>(null);
|
|
6
|
+
|
|
7
|
+
useEffect(() => {
|
|
8
|
+
callbackRef.current = callback;
|
|
9
|
+
}, [callback]);
|
|
10
|
+
|
|
11
|
+
const set = useCallback(() => {
|
|
12
|
+
timeoutRef.current = setTimeout(() => callbackRef.current(), delay);
|
|
13
|
+
}, [delay]);
|
|
14
|
+
|
|
15
|
+
const clear = useCallback(() => {
|
|
16
|
+
timeoutRef.current && clearTimeout(timeoutRef.current);
|
|
17
|
+
}, []);
|
|
18
|
+
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
set();
|
|
21
|
+
return clear;
|
|
22
|
+
}, [delay, set, clear]);
|
|
23
|
+
|
|
24
|
+
const reset = useCallback(() => {
|
|
25
|
+
clear();
|
|
26
|
+
set();
|
|
27
|
+
}, [clear, set]);
|
|
28
|
+
|
|
29
|
+
return { reset, clear };
|
|
30
|
+
}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { useAuthStore, useLoadingStore } from '@src/app/stores';
|
|
2
|
+
import axios, { AxiosRequestConfig, AxiosResponse } from 'axios';
|
|
3
|
+
import qs from 'qs';
|
|
4
|
+
import { Alert } from 'react-native';
|
|
5
|
+
import Config from 'react-native-config';
|
|
6
|
+
import { Screen } from '@src/app/navigation/app-route-type';
|
|
7
|
+
import { NavigationUtil, navigationRef } from '@src/core/utils/navigation.util';
|
|
8
|
+
import { Translator } from '@src/core/utils/translator.util';
|
|
9
|
+
import { getDeviceId } from './device-id.service';
|
|
10
|
+
import { triggerSessionEnd } from './session-end.bridge';
|
|
11
|
+
|
|
12
|
+
// Backend supports French and English; map anything else to English so OTP
|
|
13
|
+
// emails and other localized responses match the user's selected language
|
|
14
|
+
// (LIFEMASTER-369). i18n.language is the source of truth — the profile language
|
|
15
|
+
// switch drives it directly.
|
|
16
|
+
const acceptLanguage = (): string => (Translator.currentLanguage().startsWith('fr') ? 'fr' : 'en');
|
|
17
|
+
|
|
18
|
+
// API Configuration
|
|
19
|
+
export const API_VERSION = 'v1';
|
|
20
|
+
export const BaseURL = `${Config.BASE_URL}${API_VERSION}/`;
|
|
21
|
+
// Create axios instance with default configuration
|
|
22
|
+
const axiosInstance = axios.create({
|
|
23
|
+
timeout: 10000,
|
|
24
|
+
baseURL: BaseURL,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
axiosInstance.interceptors.request.use(async (config) => {
|
|
28
|
+
try {
|
|
29
|
+
const deviceId = await getDeviceId();
|
|
30
|
+
config.headers.set('X-Device-Id', deviceId);
|
|
31
|
+
} catch {
|
|
32
|
+
// Device ID resolution failed — proceed without header
|
|
33
|
+
}
|
|
34
|
+
return config;
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
let sessionExpiredAlertShown = false;
|
|
38
|
+
|
|
39
|
+
const handleSessionExpired = async () => {
|
|
40
|
+
if (sessionExpiredAlertShown) return;
|
|
41
|
+
// Full session teardown on a 401 (token, premium lease, player audio, React
|
|
42
|
+
// Query cache, library, favorites…). Routed through the bridge — the actual
|
|
43
|
+
// `endSession` lives in the app layer and pulls feature stores, so importing
|
|
44
|
+
// it here would create a core → app import cycle.
|
|
45
|
+
triggerSessionEnd();
|
|
46
|
+
// Cold launch: the navigation container isn't mounted yet (App renders null
|
|
47
|
+
// until the bootstrap route resolves), so a 401 from the bootstrap resume
|
|
48
|
+
// fetch must NOT pop a "session expired" alert over Onboarding — bootstrap
|
|
49
|
+
// already routes there. Clear silently and let it own the routing.
|
|
50
|
+
if (!navigationRef.current) return;
|
|
51
|
+
sessionExpiredAlertShown = true;
|
|
52
|
+
NavigationUtil.reset(Screen.SignIn);
|
|
53
|
+
Alert.alert(
|
|
54
|
+
Translator.translate('auth.sessionExpired.title'),
|
|
55
|
+
Translator.translate('auth.sessionExpired.message'),
|
|
56
|
+
[{
|
|
57
|
+
text: Translator.translate('auth.sessionExpired.cta'),
|
|
58
|
+
onPress: () => { sessionExpiredAlertShown = false; },
|
|
59
|
+
}],
|
|
60
|
+
{ cancelable: false, onDismiss: () => { sessionExpiredAlertShown = false; } },
|
|
61
|
+
);
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
axiosInstance.interceptors.response.use(
|
|
65
|
+
(response: AxiosResponse) => response,
|
|
66
|
+
(error) => {
|
|
67
|
+
if (error?.response?.status === 401 && useAuthStore.getState().token) {
|
|
68
|
+
handleSessionExpired();
|
|
69
|
+
}
|
|
70
|
+
return Promise.reject(error);
|
|
71
|
+
}
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
// Types
|
|
75
|
+
interface ApiError {
|
|
76
|
+
message: string;
|
|
77
|
+
code?: string;
|
|
78
|
+
success: boolean;
|
|
79
|
+
status?: number;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
interface ApiResponse<T = any> {
|
|
83
|
+
data?: T;
|
|
84
|
+
success: boolean;
|
|
85
|
+
errors?: Array<{ message: string; code?: string }>;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Error handling utility
|
|
89
|
+
const handleApiError = (error: any): ApiError => {
|
|
90
|
+
const status = error?.response?.status;
|
|
91
|
+
if (error.response?.data) {
|
|
92
|
+
const { data } = error.response;
|
|
93
|
+
const { errors, success } = data;
|
|
94
|
+
|
|
95
|
+
if (!success && errors?.length > 0) {
|
|
96
|
+
const { message, code } = errors[0];
|
|
97
|
+
return {
|
|
98
|
+
message: message?.message || message || 'An error occurred',
|
|
99
|
+
code,
|
|
100
|
+
success: false,
|
|
101
|
+
status,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return { ...data, status };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
message: error.message || 'Network error occurred',
|
|
110
|
+
success: false,
|
|
111
|
+
status,
|
|
112
|
+
};
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
// Response preprocessing utility
|
|
116
|
+
const preprocessResponse = <T>(result: ApiResponse<T>): T | ApiError => {
|
|
117
|
+
const { success, errors } = result || {};
|
|
118
|
+
|
|
119
|
+
if (success) {
|
|
120
|
+
return (result) as T;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (errors && errors.length > 0) {
|
|
124
|
+
return { ...errors[0], success: false };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return result as T;
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
export class ApiService {
|
|
131
|
+
// Get authorization headers
|
|
132
|
+
private static getAuthHeaders(): Record<string, string> {
|
|
133
|
+
const token = useAuthStore.getState().token;
|
|
134
|
+
return {
|
|
135
|
+
Accept: 'application/json',
|
|
136
|
+
'Content-Type': 'application/json',
|
|
137
|
+
'Accept-Language': acceptLanguage(),
|
|
138
|
+
...(token && { Authorization: `Bearer ${token}` }),
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Form-data headers. Content-Type is intentionally OMITTED: axios must set it
|
|
143
|
+
// itself so it appends the `; boundary=...` the server needs to split the
|
|
144
|
+
// parts. Hardcoding `multipart/form-data` (no boundary) made the backend fail
|
|
145
|
+
// to parse the file — the avatar upload then errored on some photos but not
|
|
146
|
+
// others depending on the payload (LIFEMASTER-260).
|
|
147
|
+
private static getFormDataHeaders(): Record<string, string> {
|
|
148
|
+
const token = useAuthStore.getState().token;
|
|
149
|
+
return {
|
|
150
|
+
Accept: 'application/json',
|
|
151
|
+
'Accept-Language': acceptLanguage(),
|
|
152
|
+
...(token && { Authorization: `Bearer ${token}` }),
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Generic request method. Drives the global loading overlay via the
|
|
157
|
+
// ref-counted loading store; pass `silent: true` on the config to opt out
|
|
158
|
+
// (e.g. background refetch / polling) so it doesn't block the UI.
|
|
159
|
+
private static async makeRequest<T>(
|
|
160
|
+
config: AxiosRequestConfig & { silent?: boolean },
|
|
161
|
+
): Promise<T> {
|
|
162
|
+
const { silent, ...axiosConfig } = config;
|
|
163
|
+
if (!silent) useLoadingStore.getState().showLoading();
|
|
164
|
+
try {
|
|
165
|
+
const response = await axiosInstance(axiosConfig);
|
|
166
|
+
return preprocessResponse(response.data) as T;
|
|
167
|
+
} catch (error) {
|
|
168
|
+
throw handleApiError(error);
|
|
169
|
+
} finally {
|
|
170
|
+
if (!silent) useLoadingStore.getState().hideLoading();
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// GET request — pass silent=true to skip the global loading overlay.
|
|
175
|
+
static async get<T = any>(url: string, params?: any, silent = false): Promise<IResponse<T>> {
|
|
176
|
+
return this.makeRequest<IResponse<T>>({
|
|
177
|
+
method: 'GET',
|
|
178
|
+
url,
|
|
179
|
+
headers: this.getAuthHeaders(),
|
|
180
|
+
params,
|
|
181
|
+
paramsSerializer: (p) => qs.stringify(p, { arrayFormat: 'repeat' }),
|
|
182
|
+
silent,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// POST request
|
|
187
|
+
static async post<T = any>(url: string, data?: any, isFormData = false, silent = false): Promise<IResponse<T>> {
|
|
188
|
+
return this.makeRequest<IResponse<T>>({
|
|
189
|
+
method: 'POST',
|
|
190
|
+
url,
|
|
191
|
+
headers: isFormData ? this.getFormDataHeaders() : this.getAuthHeaders(),
|
|
192
|
+
data,
|
|
193
|
+
silent,
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// PUT request
|
|
198
|
+
static async put<T = any>(url: string, data?: any, silent = false): Promise<IResponse<T>> {
|
|
199
|
+
return this.makeRequest<IResponse<T>>({
|
|
200
|
+
method: 'PUT',
|
|
201
|
+
url,
|
|
202
|
+
headers: this.getAuthHeaders(),
|
|
203
|
+
data,
|
|
204
|
+
silent,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// DELETE request
|
|
209
|
+
static async delete<T = any>(url: string, data?: any, silent = false): Promise<IResponse<T>> {
|
|
210
|
+
return this.makeRequest<IResponse<T>>({
|
|
211
|
+
method: 'DELETE',
|
|
212
|
+
url,
|
|
213
|
+
headers: this.getAuthHeaders(),
|
|
214
|
+
data,
|
|
215
|
+
silent,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// PATCH request (added for completeness)
|
|
220
|
+
static async patch<T = any>(url: string, data?: any, silent = false): Promise<IResponse<T>> {
|
|
221
|
+
return this.makeRequest<IResponse<T>>({
|
|
222
|
+
method: 'PATCH',
|
|
223
|
+
url,
|
|
224
|
+
headers: this.getAuthHeaders(),
|
|
225
|
+
data,
|
|
226
|
+
silent,
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
2
|
+
import DeviceInfo from 'react-native-device-info';
|
|
3
|
+
|
|
4
|
+
const DEVICE_ID_KEY = 'device_id';
|
|
5
|
+
|
|
6
|
+
let cachedDeviceId: string | null = null;
|
|
7
|
+
|
|
8
|
+
export const getDeviceId = async (): Promise<string> => {
|
|
9
|
+
if (cachedDeviceId) return cachedDeviceId;
|
|
10
|
+
|
|
11
|
+
const stored = await AsyncStorage.getItem(DEVICE_ID_KEY);
|
|
12
|
+
if (stored) {
|
|
13
|
+
cachedDeviceId = stored;
|
|
14
|
+
return stored;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const nativeId = await DeviceInfo.getUniqueId();
|
|
18
|
+
await AsyncStorage.setItem(DEVICE_ID_KEY, nativeId);
|
|
19
|
+
cachedDeviceId = nativeId;
|
|
20
|
+
return nativeId;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const getCachedDeviceId = (): string | null => cachedDeviceId;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session-teardown bridge.
|
|
3
|
+
*
|
|
4
|
+
* The 401 response interceptor in `api.service` must trigger a FULL session
|
|
5
|
+
* reset (tokens, premium lease, player audio, React Query cache, library,
|
|
6
|
+
* favorites…). That teardown (`endSession`) lives in the app layer and pulls in
|
|
7
|
+
* feature stores, so importing it here would create a `core → app → core` import
|
|
8
|
+
* cycle — which this codebase deliberately avoids.
|
|
9
|
+
*
|
|
10
|
+
* Instead the app registers its teardown once at startup ({@link App}) and the
|
|
11
|
+
* interceptor fires it through this shim, keeping the import graph one-way
|
|
12
|
+
* (app → core). Until something registers, {@link triggerSessionEnd} is a no-op.
|
|
13
|
+
*/
|
|
14
|
+
type SessionEndHandler = () => void;
|
|
15
|
+
|
|
16
|
+
let handler: SessionEndHandler | null = null;
|
|
17
|
+
|
|
18
|
+
/** Wire the app-level `endSession` in. Called once at app startup. */
|
|
19
|
+
export const registerSessionEndHandler = (fn: SessionEndHandler): void => {
|
|
20
|
+
handler = fn;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/** Run the registered teardown (no-op if the app hasn't registered one yet). */
|
|
24
|
+
export const triggerSessionEnd = (): void => {
|
|
25
|
+
handler?.();
|
|
26
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Colors } from '@src/core/utils';
|
|
2
|
+
import { ThemeColors } from './theme.types';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Dark palette. The existing static `Colors` object already encodes the dark
|
|
6
|
+
* look (light text on dark surfaces), so the dark theme reuses it verbatim —
|
|
7
|
+
* dark mode therefore renders identically to the pre-theme app, with zero
|
|
8
|
+
* visual regression.
|
|
9
|
+
*/
|
|
10
|
+
export const darkColors: ThemeColors = Colors;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { darkColors } from './dark.theme';
|
|
2
|
+
import { ThemeColors } from './theme.types';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Light palette. Built by spreading the dark theme and overriding only the
|
|
6
|
+
* neutral surface / text / border tokens with light values — brand tokens
|
|
7
|
+
* (primary purple, secondary blue, semantic colors) stay identical across
|
|
8
|
+
* themes so the brand reads the same in both modes.
|
|
9
|
+
*
|
|
10
|
+
* TODO(design): these light DS values are a pragmatic default derived from the
|
|
11
|
+
* existing legacy light tokens. Confirm the exact light palette against the
|
|
12
|
+
* Figma "Life-Master-Design" light variables when available.
|
|
13
|
+
*/
|
|
14
|
+
export const lightColors: ThemeColors = {
|
|
15
|
+
...darkColors,
|
|
16
|
+
|
|
17
|
+
// --- foreground (text/icon) ---
|
|
18
|
+
fg_neutral_normal: '#0F0F0F', // primary text on light surface
|
|
19
|
+
fg_neutral_faded: '#525252', // muted / disabled text
|
|
20
|
+
fg_neutral_reverse: '#FBFAFF', // text on dark/reverse fill
|
|
21
|
+
fg_neutral_placeholder: '#A8A8A8',
|
|
22
|
+
|
|
23
|
+
// --- background (fill) ---
|
|
24
|
+
bg_elevation_reverse: '#141414', // dark fill (neutral solid) on light
|
|
25
|
+
bg_elevation_level_1_normal: '#FFFFFF', // base surface
|
|
26
|
+
bg_elevation_level_2_normal: '#F5F5F5', // raised / disabled surface
|
|
27
|
+
bg_input: 'rgba(0, 0, 0, 0.06)', // input field fill on light
|
|
28
|
+
bg_disable: '#E5E5E5',
|
|
29
|
+
|
|
30
|
+
// --- border ---
|
|
31
|
+
bd_neutral_normal: '#E5E5E5',
|
|
32
|
+
bd_neutral_faded: '#C6C6C6',
|
|
33
|
+
|
|
34
|
+
// --- danger surfaces (destructive actions) — softened for light surface ---
|
|
35
|
+
bg_danger_faded: '#FDE7EC', // light danger fill (logout button bg)
|
|
36
|
+
bd_danger_faded: '#F43F5E',
|
|
37
|
+
fg_danger_faded_ds: '#BE123C', // readable danger text on light fill
|
|
38
|
+
|
|
39
|
+
// --- translucent card overlays (rows over the light background) ---
|
|
40
|
+
bg_overlay_card: 'rgba(0, 0, 0, 0.04)',
|
|
41
|
+
bg_overlay_card_medium: 'rgba(0, 0, 0, 0.05)',
|
|
42
|
+
bg_overlay_card_strong: 'rgba(0, 0, 0, 0.06)',
|
|
43
|
+
bg_progress_track: 'rgba(0, 0, 0, 0.1)',
|
|
44
|
+
};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import React, {
|
|
2
|
+
createContext,
|
|
3
|
+
PropsWithChildren,
|
|
4
|
+
useCallback,
|
|
5
|
+
useContext,
|
|
6
|
+
useEffect,
|
|
7
|
+
useMemo,
|
|
8
|
+
useState,
|
|
9
|
+
} from 'react';
|
|
10
|
+
import { useColorScheme } from 'react-native';
|
|
11
|
+
import { AppStorageUtil, StorageKeys } from '@src/core/utils';
|
|
12
|
+
import { darkColors } from './dark.theme';
|
|
13
|
+
import { lightColors } from './light.theme';
|
|
14
|
+
import {
|
|
15
|
+
ResolvedScheme,
|
|
16
|
+
ThemeContextValue,
|
|
17
|
+
ThemeScheme,
|
|
18
|
+
} from './theme.types';
|
|
19
|
+
|
|
20
|
+
// Only the dark theme is shipped for now — the light palette is a draft
|
|
21
|
+
// pending design. Default to dark and (below) lock the resolved scheme to dark
|
|
22
|
+
// so the OS/light never leaks through. To re-enable light later: set this back
|
|
23
|
+
// to 'system' and remove the `LIGHT_THEME_ENABLED` lock in `resolved`.
|
|
24
|
+
const DEFAULT_SCHEME: ThemeScheme = 'dark';
|
|
25
|
+
const LIGHT_THEME_ENABLED = false;
|
|
26
|
+
|
|
27
|
+
const ThemeContext = createContext<ThemeContextValue | undefined>(undefined);
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Provides theme tokens app-wide. Defaults to following the OS appearance and
|
|
31
|
+
* hydrates the persisted user override (AsyncStorage) after mount. Mount this
|
|
32
|
+
* once near the app root, above the navigation tree.
|
|
33
|
+
*/
|
|
34
|
+
export const ThemeProvider = ({ children }: PropsWithChildren) => {
|
|
35
|
+
const osScheme = useColorScheme();
|
|
36
|
+
const [scheme, setSchemeState] = useState<ThemeScheme>(DEFAULT_SCHEME);
|
|
37
|
+
|
|
38
|
+
// Hydrate the persisted preference once on mount (AsyncStorage is async, so
|
|
39
|
+
// the first frame uses the system default and corrects itself here).
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
let active = true;
|
|
42
|
+
AppStorageUtil.getItem(StorageKeys.THEME).then(saved => {
|
|
43
|
+
if (active && (saved === 'light' || saved === 'dark' || saved === 'system')) {
|
|
44
|
+
setSchemeState(saved);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
return () => {
|
|
48
|
+
active = false;
|
|
49
|
+
};
|
|
50
|
+
}, []);
|
|
51
|
+
|
|
52
|
+
const setScheme = useCallback((next: ThemeScheme) => {
|
|
53
|
+
setSchemeState(next);
|
|
54
|
+
AppStorageUtil.setItem(StorageKeys.THEME, next);
|
|
55
|
+
}, []);
|
|
56
|
+
|
|
57
|
+
const requested: ResolvedScheme =
|
|
58
|
+
scheme === 'system' ? (osScheme === 'dark' ? 'dark' : 'light') : scheme;
|
|
59
|
+
// Light theme is not shipped yet — force dark until it is.
|
|
60
|
+
const resolved: ResolvedScheme = LIGHT_THEME_ENABLED ? requested : 'dark';
|
|
61
|
+
|
|
62
|
+
const value = useMemo<ThemeContextValue>(
|
|
63
|
+
() => ({
|
|
64
|
+
colors: resolved === 'dark' ? darkColors : lightColors,
|
|
65
|
+
scheme,
|
|
66
|
+
resolved,
|
|
67
|
+
setScheme,
|
|
68
|
+
}),
|
|
69
|
+
[resolved, scheme, setScheme],
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/** Access the active theme. Throws if used outside `ThemeProvider`. */
|
|
76
|
+
export const useTheme = (): ThemeContextValue => {
|
|
77
|
+
const ctx = useContext(ThemeContext);
|
|
78
|
+
if (!ctx) {
|
|
79
|
+
throw new Error('useTheme must be used within a ThemeProvider');
|
|
80
|
+
}
|
|
81
|
+
return ctx;
|
|
82
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Colors } from '@src/core/utils';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The theme color contract. Both the light and dark palettes must expose the
|
|
5
|
+
* exact same keys as the static `Colors` object, so design-system components
|
|
6
|
+
* can keep referencing token names (`colors.bg_primary_normal`) unchanged —
|
|
7
|
+
* only the *source* of the value switches with the active theme.
|
|
8
|
+
*/
|
|
9
|
+
export type ThemeColors = typeof Colors;
|
|
10
|
+
|
|
11
|
+
/** What the user can pick: an explicit theme or "follow the OS". */
|
|
12
|
+
export type ThemeScheme = 'light' | 'dark' | 'system';
|
|
13
|
+
|
|
14
|
+
/** The concrete theme actually rendered after resolving `system`. */
|
|
15
|
+
export type ResolvedScheme = 'light' | 'dark';
|
|
16
|
+
|
|
17
|
+
export interface ThemeContextValue {
|
|
18
|
+
/** Active color tokens for the resolved scheme. */
|
|
19
|
+
colors: ThemeColors;
|
|
20
|
+
/** The user's preference (may be `system`). */
|
|
21
|
+
scheme: ThemeScheme;
|
|
22
|
+
/** The concrete scheme being rendered (`system` already resolved). */
|
|
23
|
+
resolved: ResolvedScheme;
|
|
24
|
+
/** Persist and apply a new preference. */
|
|
25
|
+
setScheme: (next: ThemeScheme) => void;
|
|
26
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
import { useTheme } from './theme-context';
|
|
3
|
+
import { ThemeColors } from './theme.types';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Build a themed StyleSheet that recomputes only when the active theme flips.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* ```ts
|
|
10
|
+
* const makeStyles = (c: ThemeColors) =>
|
|
11
|
+
* StyleSheet.create({ box: { backgroundColor: c.bg_elevation_level_1_normal } });
|
|
12
|
+
*
|
|
13
|
+
* const Component = () => {
|
|
14
|
+
* const styles = useThemedStyles(makeStyles);
|
|
15
|
+
* return <View style={styles.box} />;
|
|
16
|
+
* };
|
|
17
|
+
* ```
|
|
18
|
+
*
|
|
19
|
+
* The factory must be a stable module-scope function (not an inline arrow) so
|
|
20
|
+
* the memo key stays the theme reference, not a new function each render.
|
|
21
|
+
*/
|
|
22
|
+
export const useThemedStyles = <T>(factory: (colors: ThemeColors) => T): T => {
|
|
23
|
+
const { colors } = useTheme();
|
|
24
|
+
return useMemo(() => factory(colors), [colors, factory]);
|
|
25
|
+
};
|