From d93e4d9c9f40ca478f2bc2bab73b2787f2edebd5 Mon Sep 17 00:00:00 2001 From: Sofio Date: Thu, 5 Feb 2026 13:16:05 +0800 Subject: [PATCH] template_0205 --- RN_TEMPLATE/.dependency-cruiser.js | 195 + RN_TEMPLATE/.env | 6 + RN_TEMPLATE/.eslintignore | 8 + RN_TEMPLATE/.eslintrc.js | 105 + RN_TEMPLATE/.gitignore | 92 + .../.maestro/flows/FavoritePodcast.yaml | 37 + RN_TEMPLATE/.maestro/flows/Login.yaml | 13 + RN_TEMPLATE/.maestro/shared/_Login.yaml | 12 + RN_TEMPLATE/.maestro/shared/_OnFlowStart.yaml | 39 + RN_TEMPLATE/.npmrc | 2 + RN_TEMPLATE/.prettierignore | 9 + RN_TEMPLATE/.prettierrc | 7 + RN_TEMPLATE/CLAUDE.md | 314 + RN_TEMPLATE/README.md | 77 + RN_TEMPLATE/app.config.ts | 50 + RN_TEMPLATE/app.json | 71 + RN_TEMPLATE/app/app.tsx | 117 + RN_TEMPLATE/app/components/AutoImage.tsx | 89 + RN_TEMPLATE/app/components/Avatar.tsx | 71 + RN_TEMPLATE/app/components/Button.tsx | 349 + RN_TEMPLATE/app/components/Card.tsx | 315 + RN_TEMPLATE/app/components/Dialog.tsx | 276 + RN_TEMPLATE/app/components/EmptyState.tsx | 249 + RN_TEMPLATE/app/components/Header.tsx | 337 + RN_TEMPLATE/app/components/Icon.tsx | 198 + RN_TEMPLATE/app/components/ListItem.tsx | 257 + RN_TEMPLATE/app/components/Modal.tsx | 386 + RN_TEMPLATE/app/components/Screen.tsx | 306 + RN_TEMPLATE/app/components/Skeleton.tsx | 120 + RN_TEMPLATE/app/components/Text.test.tsx | 23 + RN_TEMPLATE/app/components/Text.tsx | 118 + RN_TEMPLATE/app/components/TextField.tsx | 292 + .../app/components/Toggle/Checkbox.tsx | 106 + RN_TEMPLATE/app/components/Toggle/Radio.tsx | 107 + RN_TEMPLATE/app/components/Toggle/Switch.tsx | 256 + RN_TEMPLATE/app/components/Toggle/Toggle.tsx | 286 + RN_TEMPLATE/app/config/config.base.ts | 26 + RN_TEMPLATE/app/config/config.dev.ts | 16 + RN_TEMPLATE/app/config/config.prod.ts | 16 + RN_TEMPLATE/app/config/index.ts | 28 + RN_TEMPLATE/app/context/AuthContext.tsx | 1223 ++ RN_TEMPLATE/app/context/EpisodeContext.tsx | 134 + RN_TEMPLATE/app/context/ToastContext.tsx | 228 + RN_TEMPLATE/app/devtools/ReactotronClient.ts | 9 + .../app/devtools/ReactotronClient.web.ts | 12 + RN_TEMPLATE/app/devtools/ReactotronConfig.ts | 139 + RN_TEMPLATE/app/i18n/ar.ts | 297 + RN_TEMPLATE/app/i18n/demo-ar.ts | 462 + RN_TEMPLATE/app/i18n/demo-en.ts | 460 + RN_TEMPLATE/app/i18n/demo-es.ts | 467 + RN_TEMPLATE/app/i18n/demo-fr.ts | 469 + RN_TEMPLATE/app/i18n/demo-hi.ts | 466 + RN_TEMPLATE/app/i18n/demo-ja.ts | 462 + RN_TEMPLATE/app/i18n/demo-ko.ts | 455 + RN_TEMPLATE/app/i18n/demo-zh.ts | 442 + RN_TEMPLATE/app/i18n/en.ts | 306 + RN_TEMPLATE/app/i18n/es.ts | 303 + RN_TEMPLATE/app/i18n/fr.ts | 303 + RN_TEMPLATE/app/i18n/hi.ts | 301 + RN_TEMPLATE/app/i18n/index.ts | 87 + RN_TEMPLATE/app/i18n/ja.ts | 301 + RN_TEMPLATE/app/i18n/ko.ts | 300 + RN_TEMPLATE/app/i18n/translate.ts | 33 + RN_TEMPLATE/app/i18n/zh.ts | 297 + RN_TEMPLATE/app/navigators/AppNavigator.tsx | 103 + RN_TEMPLATE/app/navigators/MainNavigator.tsx | 350 + RN_TEMPLATE/app/navigators/navigationTypes.ts | 49 + .../app/navigators/navigationUtilities.ts | 208 + RN_TEMPLATE/app/screens/AboutScreen.tsx | 217 + RN_TEMPLATE/app/screens/AuthWelcomeScreen.tsx | 88 + RN_TEMPLATE/app/screens/ChangeEmailScreen.tsx | 540 + .../app/screens/ChangePasswordScreen.tsx | 216 + RN_TEMPLATE/app/screens/CommunityScreen.tsx | 70 + .../app/screens/ErrorScreen/ErrorBoundary.tsx | 76 + .../app/screens/ErrorScreen/ErrorDetails.tsx | 99 + .../app/screens/ForgotPasswordScreen.tsx | 351 + RN_TEMPLATE/app/screens/LanguageScreen.tsx | 108 + RN_TEMPLATE/app/screens/LoginScreen.tsx | 430 + RN_TEMPLATE/app/screens/ProfileScreen.tsx | 556 + RN_TEMPLATE/app/screens/RegisterScreen.tsx | 421 + RN_TEMPLATE/app/screens/SecurityScreen.tsx | 81 + .../app/screens/SessionManagementScreen.tsx | 515 + RN_TEMPLATE/app/screens/SettingsScreen.tsx | 170 + .../screens/ShowroomScreen/DemoDivider.tsx | 65 + .../screens/ShowroomScreen/DemoUseCase.tsx | 51 + .../ShowroomScreen/DrawerIconButton.tsx | 119 + ...SectionListWithKeyboardAwareScrollView.tsx | 57 + .../screens/ShowroomScreen/ShowroomScreen.tsx | 320 + .../ShowroomScreen/demos/DemoAutoImage.tsx | 229 + .../ShowroomScreen/demos/DemoButton.tsx | 226 + .../screens/ShowroomScreen/demos/DemoCard.tsx | 180 + .../ShowroomScreen/demos/DemoEmptyState.tsx | 77 + .../ShowroomScreen/demos/DemoHeader.tsx | 150 + .../screens/ShowroomScreen/demos/DemoIcon.tsx | 109 + .../ShowroomScreen/demos/DemoListItem.tsx | 217 + .../screens/ShowroomScreen/demos/DemoText.tsx | 142 + .../ShowroomScreen/demos/DemoTextField.tsx | 231 + .../ShowroomScreen/demos/DemoToggle.tsx | 348 + .../app/screens/ShowroomScreen/demos/index.ts | 10 + .../app/screens/ShowroomScreen/demos/types.ts | 10 + RN_TEMPLATE/app/screens/ThemeScreen.tsx | 108 + RN_TEMPLATE/app/screens/WelcomeScreen.tsx | 111 + .../app/services/api/apiProblem.test.ts | 73 + RN_TEMPLATE/app/services/api/apiProblem.ts | 103 + RN_TEMPLATE/app/services/api/authApi.ts | 878 + RN_TEMPLATE/app/services/api/authTypes.ts | 421 + RN_TEMPLATE/app/services/api/index.ts | 82 + RN_TEMPLATE/app/services/api/types.ts | 50 + RN_TEMPLATE/app/services/api/uploadApi.ts | 150 + RN_TEMPLATE/app/theme/colors.ts | 85 + RN_TEMPLATE/app/theme/colorsDark.ts | 50 + RN_TEMPLATE/app/theme/context.tsx | 145 + RN_TEMPLATE/app/theme/context.utils.ts | 25 + RN_TEMPLATE/app/theme/spacing.ts | 16 + RN_TEMPLATE/app/theme/spacingDark.ts | 16 + RN_TEMPLATE/app/theme/styles.ts | 23 + RN_TEMPLATE/app/theme/theme.ts | 23 + RN_TEMPLATE/app/theme/timing.ts | 6 + RN_TEMPLATE/app/theme/types.ts | 64 + RN_TEMPLATE/app/theme/typography.ts | 71 + RN_TEMPLATE/app/utils/authErrorTranslator.ts | 81 + RN_TEMPLATE/app/utils/crashReporting.ts | 62 + RN_TEMPLATE/app/utils/delay.ts | 6 + RN_TEMPLATE/app/utils/formatDate.ts | 49 + .../app/utils/gestureHandler.native.ts | 3 + RN_TEMPLATE/app/utils/gestureHandler.ts | 6 + RN_TEMPLATE/app/utils/openLinkInBrowser.ts | 8 + RN_TEMPLATE/app/utils/responsive.ts | 14 + RN_TEMPLATE/app/utils/storage/index.ts | 82 + RN_TEMPLATE/app/utils/storage/storage.test.ts | 61 + RN_TEMPLATE/app/utils/useHeader.tsx | 39 + RN_TEMPLATE/app/utils/useIsMounted.ts | 18 + .../app/utils/useSafeAreaInsetsStyle.ts | 46 + RN_TEMPLATE/assets/images/app-icon-all.png | Bin 0 -> 46593 bytes .../app-icon-android-adaptive-background.png | Bin 0 -> 16049 bytes .../app-icon-android-adaptive-foreground.png | Bin 0 -> 29653 bytes .../assets/images/app-icon-android-legacy.png | Bin 0 -> 47201 bytes RN_TEMPLATE/assets/images/app-icon-ios.png | Bin 0 -> 46593 bytes .../assets/images/app-icon-web-favicon.png | Bin 0 -> 2169 bytes RN_TEMPLATE/assets/images/demo/cr-logo.png | Bin 0 -> 2428 bytes RN_TEMPLATE/assets/images/demo/cr-logo@2x.png | Bin 0 -> 5869 bytes RN_TEMPLATE/assets/images/demo/cr-logo@3x.png | Bin 0 -> 9217 bytes RN_TEMPLATE/assets/images/demo/rnl-logo.png | Bin 0 -> 2377 bytes .../assets/images/demo/rnl-logo@2x.png | Bin 0 -> 5258 bytes .../assets/images/demo/rnl-logo@3x.png | Bin 0 -> 8005 bytes RN_TEMPLATE/assets/images/demo/rnn-logo.png | Bin 0 -> 2455 bytes .../assets/images/demo/rnn-logo@2x.png | Bin 0 -> 5762 bytes .../assets/images/demo/rnn-logo@3x.png | Bin 0 -> 8903 bytes .../assets/images/demo/rnr-image-1.png | Bin 0 -> 1774 bytes .../assets/images/demo/rnr-image-1@2x.png | Bin 0 -> 3929 bytes .../assets/images/demo/rnr-image-1@3x.png | Bin 0 -> 6422 bytes .../assets/images/demo/rnr-image-2.png | Bin 0 -> 1966 bytes .../assets/images/demo/rnr-image-2@2x.png | Bin 0 -> 4323 bytes .../assets/images/demo/rnr-image-2@3x.png | Bin 0 -> 7220 bytes .../assets/images/demo/rnr-image-3.png | Bin 0 -> 1570 bytes .../assets/images/demo/rnr-image-3@2x.png | Bin 0 -> 3358 bytes .../assets/images/demo/rnr-image-3@3x.png | Bin 0 -> 5404 bytes RN_TEMPLATE/assets/images/demo/rnr-logo.png | Bin 0 -> 2680 bytes .../assets/images/demo/rnr-logo@2x.png | Bin 0 -> 6009 bytes .../assets/images/demo/rnr-logo@3x.png | Bin 0 -> 9243 bytes RN_TEMPLATE/assets/images/google-logo.png | Bin 0 -> 3428 bytes RN_TEMPLATE/assets/images/logo.png | Bin 0 -> 8956 bytes RN_TEMPLATE/assets/images/logo@2x.png | Bin 0 -> 18839 bytes RN_TEMPLATE/assets/images/logo@3x.png | Bin 0 -> 29903 bytes RN_TEMPLATE/assets/images/sad-face.png | Bin 0 -> 11830 bytes RN_TEMPLATE/assets/images/sad-face@2x.png | Bin 0 -> 36321 bytes RN_TEMPLATE/assets/images/sad-face@3x.png | Bin 0 -> 75212 bytes RN_TEMPLATE/assets/images/welcome-face.png | Bin 0 -> 12129 bytes RN_TEMPLATE/assets/images/welcome-face@2x.png | Bin 0 -> 37275 bytes RN_TEMPLATE/assets/images/welcome-face@3x.png | Bin 0 -> 77799 bytes RN_TEMPLATE/babel.config.js | 7 + RN_TEMPLATE/bun.lock | 2749 +++ RN_TEMPLATE/eas.json | 47 + RN_TEMPLATE/global.css | 3 + .../app-icon/android-adaptive-background.png | Bin 0 -> 15862 bytes .../app-icon/android-adaptive-foreground.png | Bin 0 -> 56135 bytes .../templates/app-icon/android-legacy.png | Bin 0 -> 56565 bytes .../templates/app-icon/ios-universal.png | Bin 0 -> 55883 bytes .../ignite/templates/component/NAME.tsx.ejs | 39 + .../templates/navigator/NAMENavigator.tsx.ejs | 18 + .../templates/screen/NAMEScreen.tsx.ejs | 29 + .../ignite/templates/splash-screen/logo.png | Bin 0 -> 79934 bytes RN_TEMPLATE/index.tsx | 10 + RN_TEMPLATE/jest.config.js | 5 + RN_TEMPLATE/metro.config.js | 31 + RN_TEMPLATE/nativewind-env.d.ts | 1 + RN_TEMPLATE/package-lock.json | 14135 ++++++++++++++++ RN_TEMPLATE/package.json | 116 + RN_TEMPLATE/pnpm-lock.yaml | 11880 +++++++++++++ RN_TEMPLATE/sha1.md | 1 + RN_TEMPLATE/tailwind.config.js | 13 + RN_TEMPLATE/test/i18n.test.ts | 75 + RN_TEMPLATE/test/mockFile.ts | 6 + RN_TEMPLATE/test/setup.ts | 58 + RN_TEMPLATE/test/test-tsconfig.json | 8 + RN_TEMPLATE/tsconfig.json | 53 + RN_TEMPLATE/types/lib.es5.d.ts | 25 + 197 files changed, 52810 insertions(+) create mode 100644 RN_TEMPLATE/.dependency-cruiser.js create mode 100644 RN_TEMPLATE/.env create mode 100644 RN_TEMPLATE/.eslintignore create mode 100644 RN_TEMPLATE/.eslintrc.js create mode 100644 RN_TEMPLATE/.gitignore create mode 100644 RN_TEMPLATE/.maestro/flows/FavoritePodcast.yaml create mode 100644 RN_TEMPLATE/.maestro/flows/Login.yaml create mode 100644 RN_TEMPLATE/.maestro/shared/_Login.yaml create mode 100644 RN_TEMPLATE/.maestro/shared/_OnFlowStart.yaml create mode 100644 RN_TEMPLATE/.npmrc create mode 100644 RN_TEMPLATE/.prettierignore create mode 100644 RN_TEMPLATE/.prettierrc create mode 100644 RN_TEMPLATE/CLAUDE.md create mode 100644 RN_TEMPLATE/README.md create mode 100644 RN_TEMPLATE/app.config.ts create mode 100644 RN_TEMPLATE/app.json create mode 100644 RN_TEMPLATE/app/app.tsx create mode 100644 RN_TEMPLATE/app/components/AutoImage.tsx create mode 100644 RN_TEMPLATE/app/components/Avatar.tsx create mode 100644 RN_TEMPLATE/app/components/Button.tsx create mode 100644 RN_TEMPLATE/app/components/Card.tsx create mode 100644 RN_TEMPLATE/app/components/Dialog.tsx create mode 100644 RN_TEMPLATE/app/components/EmptyState.tsx create mode 100644 RN_TEMPLATE/app/components/Header.tsx create mode 100644 RN_TEMPLATE/app/components/Icon.tsx create mode 100644 RN_TEMPLATE/app/components/ListItem.tsx create mode 100644 RN_TEMPLATE/app/components/Modal.tsx create mode 100644 RN_TEMPLATE/app/components/Screen.tsx create mode 100644 RN_TEMPLATE/app/components/Skeleton.tsx create mode 100644 RN_TEMPLATE/app/components/Text.test.tsx create mode 100644 RN_TEMPLATE/app/components/Text.tsx create mode 100644 RN_TEMPLATE/app/components/TextField.tsx create mode 100644 RN_TEMPLATE/app/components/Toggle/Checkbox.tsx create mode 100644 RN_TEMPLATE/app/components/Toggle/Radio.tsx create mode 100644 RN_TEMPLATE/app/components/Toggle/Switch.tsx create mode 100644 RN_TEMPLATE/app/components/Toggle/Toggle.tsx create mode 100644 RN_TEMPLATE/app/config/config.base.ts create mode 100644 RN_TEMPLATE/app/config/config.dev.ts create mode 100644 RN_TEMPLATE/app/config/config.prod.ts create mode 100644 RN_TEMPLATE/app/config/index.ts create mode 100644 RN_TEMPLATE/app/context/AuthContext.tsx create mode 100644 RN_TEMPLATE/app/context/EpisodeContext.tsx create mode 100644 RN_TEMPLATE/app/context/ToastContext.tsx create mode 100644 RN_TEMPLATE/app/devtools/ReactotronClient.ts create mode 100644 RN_TEMPLATE/app/devtools/ReactotronClient.web.ts create mode 100644 RN_TEMPLATE/app/devtools/ReactotronConfig.ts create mode 100644 RN_TEMPLATE/app/i18n/ar.ts create mode 100644 RN_TEMPLATE/app/i18n/demo-ar.ts create mode 100644 RN_TEMPLATE/app/i18n/demo-en.ts create mode 100644 RN_TEMPLATE/app/i18n/demo-es.ts create mode 100644 RN_TEMPLATE/app/i18n/demo-fr.ts create mode 100644 RN_TEMPLATE/app/i18n/demo-hi.ts create mode 100644 RN_TEMPLATE/app/i18n/demo-ja.ts create mode 100644 RN_TEMPLATE/app/i18n/demo-ko.ts create mode 100644 RN_TEMPLATE/app/i18n/demo-zh.ts create mode 100644 RN_TEMPLATE/app/i18n/en.ts create mode 100644 RN_TEMPLATE/app/i18n/es.ts create mode 100644 RN_TEMPLATE/app/i18n/fr.ts create mode 100644 RN_TEMPLATE/app/i18n/hi.ts create mode 100644 RN_TEMPLATE/app/i18n/index.ts create mode 100644 RN_TEMPLATE/app/i18n/ja.ts create mode 100644 RN_TEMPLATE/app/i18n/ko.ts create mode 100644 RN_TEMPLATE/app/i18n/translate.ts create mode 100644 RN_TEMPLATE/app/i18n/zh.ts create mode 100644 RN_TEMPLATE/app/navigators/AppNavigator.tsx create mode 100644 RN_TEMPLATE/app/navigators/MainNavigator.tsx create mode 100644 RN_TEMPLATE/app/navigators/navigationTypes.ts create mode 100644 RN_TEMPLATE/app/navigators/navigationUtilities.ts create mode 100644 RN_TEMPLATE/app/screens/AboutScreen.tsx create mode 100644 RN_TEMPLATE/app/screens/AuthWelcomeScreen.tsx create mode 100644 RN_TEMPLATE/app/screens/ChangeEmailScreen.tsx create mode 100644 RN_TEMPLATE/app/screens/ChangePasswordScreen.tsx create mode 100644 RN_TEMPLATE/app/screens/CommunityScreen.tsx create mode 100644 RN_TEMPLATE/app/screens/ErrorScreen/ErrorBoundary.tsx create mode 100644 RN_TEMPLATE/app/screens/ErrorScreen/ErrorDetails.tsx create mode 100644 RN_TEMPLATE/app/screens/ForgotPasswordScreen.tsx create mode 100644 RN_TEMPLATE/app/screens/LanguageScreen.tsx create mode 100644 RN_TEMPLATE/app/screens/LoginScreen.tsx create mode 100644 RN_TEMPLATE/app/screens/ProfileScreen.tsx create mode 100644 RN_TEMPLATE/app/screens/RegisterScreen.tsx create mode 100644 RN_TEMPLATE/app/screens/SecurityScreen.tsx create mode 100644 RN_TEMPLATE/app/screens/SessionManagementScreen.tsx create mode 100644 RN_TEMPLATE/app/screens/SettingsScreen.tsx create mode 100644 RN_TEMPLATE/app/screens/ShowroomScreen/DemoDivider.tsx create mode 100644 RN_TEMPLATE/app/screens/ShowroomScreen/DemoUseCase.tsx create mode 100644 RN_TEMPLATE/app/screens/ShowroomScreen/DrawerIconButton.tsx create mode 100644 RN_TEMPLATE/app/screens/ShowroomScreen/SectionListWithKeyboardAwareScrollView.tsx create mode 100644 RN_TEMPLATE/app/screens/ShowroomScreen/ShowroomScreen.tsx create mode 100644 RN_TEMPLATE/app/screens/ShowroomScreen/demos/DemoAutoImage.tsx create mode 100644 RN_TEMPLATE/app/screens/ShowroomScreen/demos/DemoButton.tsx create mode 100644 RN_TEMPLATE/app/screens/ShowroomScreen/demos/DemoCard.tsx create mode 100644 RN_TEMPLATE/app/screens/ShowroomScreen/demos/DemoEmptyState.tsx create mode 100644 RN_TEMPLATE/app/screens/ShowroomScreen/demos/DemoHeader.tsx create mode 100644 RN_TEMPLATE/app/screens/ShowroomScreen/demos/DemoIcon.tsx create mode 100644 RN_TEMPLATE/app/screens/ShowroomScreen/demos/DemoListItem.tsx create mode 100644 RN_TEMPLATE/app/screens/ShowroomScreen/demos/DemoText.tsx create mode 100644 RN_TEMPLATE/app/screens/ShowroomScreen/demos/DemoTextField.tsx create mode 100644 RN_TEMPLATE/app/screens/ShowroomScreen/demos/DemoToggle.tsx create mode 100644 RN_TEMPLATE/app/screens/ShowroomScreen/demos/index.ts create mode 100644 RN_TEMPLATE/app/screens/ShowroomScreen/demos/types.ts create mode 100644 RN_TEMPLATE/app/screens/ThemeScreen.tsx create mode 100644 RN_TEMPLATE/app/screens/WelcomeScreen.tsx create mode 100644 RN_TEMPLATE/app/services/api/apiProblem.test.ts create mode 100644 RN_TEMPLATE/app/services/api/apiProblem.ts create mode 100644 RN_TEMPLATE/app/services/api/authApi.ts create mode 100644 RN_TEMPLATE/app/services/api/authTypes.ts create mode 100644 RN_TEMPLATE/app/services/api/index.ts create mode 100644 RN_TEMPLATE/app/services/api/types.ts create mode 100644 RN_TEMPLATE/app/services/api/uploadApi.ts create mode 100644 RN_TEMPLATE/app/theme/colors.ts create mode 100644 RN_TEMPLATE/app/theme/colorsDark.ts create mode 100644 RN_TEMPLATE/app/theme/context.tsx create mode 100644 RN_TEMPLATE/app/theme/context.utils.ts create mode 100644 RN_TEMPLATE/app/theme/spacing.ts create mode 100644 RN_TEMPLATE/app/theme/spacingDark.ts create mode 100644 RN_TEMPLATE/app/theme/styles.ts create mode 100644 RN_TEMPLATE/app/theme/theme.ts create mode 100644 RN_TEMPLATE/app/theme/timing.ts create mode 100644 RN_TEMPLATE/app/theme/types.ts create mode 100644 RN_TEMPLATE/app/theme/typography.ts create mode 100644 RN_TEMPLATE/app/utils/authErrorTranslator.ts create mode 100644 RN_TEMPLATE/app/utils/crashReporting.ts create mode 100644 RN_TEMPLATE/app/utils/delay.ts create mode 100644 RN_TEMPLATE/app/utils/formatDate.ts create mode 100644 RN_TEMPLATE/app/utils/gestureHandler.native.ts create mode 100644 RN_TEMPLATE/app/utils/gestureHandler.ts create mode 100644 RN_TEMPLATE/app/utils/openLinkInBrowser.ts create mode 100644 RN_TEMPLATE/app/utils/responsive.ts create mode 100644 RN_TEMPLATE/app/utils/storage/index.ts create mode 100644 RN_TEMPLATE/app/utils/storage/storage.test.ts create mode 100644 RN_TEMPLATE/app/utils/useHeader.tsx create mode 100644 RN_TEMPLATE/app/utils/useIsMounted.ts create mode 100644 RN_TEMPLATE/app/utils/useSafeAreaInsetsStyle.ts create mode 100644 RN_TEMPLATE/assets/images/app-icon-all.png create mode 100644 RN_TEMPLATE/assets/images/app-icon-android-adaptive-background.png create mode 100644 RN_TEMPLATE/assets/images/app-icon-android-adaptive-foreground.png create mode 100644 RN_TEMPLATE/assets/images/app-icon-android-legacy.png create mode 100644 RN_TEMPLATE/assets/images/app-icon-ios.png create mode 100644 RN_TEMPLATE/assets/images/app-icon-web-favicon.png create mode 100644 RN_TEMPLATE/assets/images/demo/cr-logo.png create mode 100644 RN_TEMPLATE/assets/images/demo/cr-logo@2x.png create mode 100644 RN_TEMPLATE/assets/images/demo/cr-logo@3x.png create mode 100644 RN_TEMPLATE/assets/images/demo/rnl-logo.png create mode 100644 RN_TEMPLATE/assets/images/demo/rnl-logo@2x.png create mode 100644 RN_TEMPLATE/assets/images/demo/rnl-logo@3x.png create mode 100644 RN_TEMPLATE/assets/images/demo/rnn-logo.png create mode 100644 RN_TEMPLATE/assets/images/demo/rnn-logo@2x.png create mode 100644 RN_TEMPLATE/assets/images/demo/rnn-logo@3x.png create mode 100644 RN_TEMPLATE/assets/images/demo/rnr-image-1.png create mode 100644 RN_TEMPLATE/assets/images/demo/rnr-image-1@2x.png create mode 100644 RN_TEMPLATE/assets/images/demo/rnr-image-1@3x.png create mode 100644 RN_TEMPLATE/assets/images/demo/rnr-image-2.png create mode 100644 RN_TEMPLATE/assets/images/demo/rnr-image-2@2x.png create mode 100644 RN_TEMPLATE/assets/images/demo/rnr-image-2@3x.png create mode 100644 RN_TEMPLATE/assets/images/demo/rnr-image-3.png create mode 100644 RN_TEMPLATE/assets/images/demo/rnr-image-3@2x.png create mode 100644 RN_TEMPLATE/assets/images/demo/rnr-image-3@3x.png create mode 100644 RN_TEMPLATE/assets/images/demo/rnr-logo.png create mode 100644 RN_TEMPLATE/assets/images/demo/rnr-logo@2x.png create mode 100644 RN_TEMPLATE/assets/images/demo/rnr-logo@3x.png create mode 100644 RN_TEMPLATE/assets/images/google-logo.png create mode 100644 RN_TEMPLATE/assets/images/logo.png create mode 100644 RN_TEMPLATE/assets/images/logo@2x.png create mode 100644 RN_TEMPLATE/assets/images/logo@3x.png create mode 100644 RN_TEMPLATE/assets/images/sad-face.png create mode 100644 RN_TEMPLATE/assets/images/sad-face@2x.png create mode 100644 RN_TEMPLATE/assets/images/sad-face@3x.png create mode 100644 RN_TEMPLATE/assets/images/welcome-face.png create mode 100644 RN_TEMPLATE/assets/images/welcome-face@2x.png create mode 100644 RN_TEMPLATE/assets/images/welcome-face@3x.png create mode 100644 RN_TEMPLATE/babel.config.js create mode 100644 RN_TEMPLATE/bun.lock create mode 100644 RN_TEMPLATE/eas.json create mode 100644 RN_TEMPLATE/global.css create mode 100644 RN_TEMPLATE/ignite/templates/app-icon/android-adaptive-background.png create mode 100644 RN_TEMPLATE/ignite/templates/app-icon/android-adaptive-foreground.png create mode 100644 RN_TEMPLATE/ignite/templates/app-icon/android-legacy.png create mode 100644 RN_TEMPLATE/ignite/templates/app-icon/ios-universal.png create mode 100644 RN_TEMPLATE/ignite/templates/component/NAME.tsx.ejs create mode 100644 RN_TEMPLATE/ignite/templates/navigator/NAMENavigator.tsx.ejs create mode 100644 RN_TEMPLATE/ignite/templates/screen/NAMEScreen.tsx.ejs create mode 100644 RN_TEMPLATE/ignite/templates/splash-screen/logo.png create mode 100644 RN_TEMPLATE/index.tsx create mode 100644 RN_TEMPLATE/jest.config.js create mode 100644 RN_TEMPLATE/metro.config.js create mode 100644 RN_TEMPLATE/nativewind-env.d.ts create mode 100644 RN_TEMPLATE/package-lock.json create mode 100644 RN_TEMPLATE/package.json create mode 100644 RN_TEMPLATE/pnpm-lock.yaml create mode 100644 RN_TEMPLATE/sha1.md create mode 100644 RN_TEMPLATE/tailwind.config.js create mode 100644 RN_TEMPLATE/test/i18n.test.ts create mode 100644 RN_TEMPLATE/test/mockFile.ts create mode 100644 RN_TEMPLATE/test/setup.ts create mode 100644 RN_TEMPLATE/test/test-tsconfig.json create mode 100644 RN_TEMPLATE/tsconfig.json create mode 100644 RN_TEMPLATE/types/lib.es5.d.ts diff --git a/RN_TEMPLATE/.dependency-cruiser.js b/RN_TEMPLATE/.dependency-cruiser.js new file mode 100644 index 0000000..7313a0b --- /dev/null +++ b/RN_TEMPLATE/.dependency-cruiser.js @@ -0,0 +1,195 @@ +const metroConfig = require("./metro.config.js") + +const platforms = ["ios", "android", "web", "native"] +const extensions = metroConfig?.resolver?.sourceExts.flatMap((pExt) => + platforms.map((platform) => `.${platform}.${pExt}`).concat(`.${pExt}`), +) + +/** @type {import('dependency-cruiser').IConfiguration} */ +module.exports = { + forbidden: [ + { + name: "no-circular", + severity: "warn", + comment: + "This dependency is part of a circular relationship. You might want to revise " + + "your solution (i.e. use dependency inversion, make sure the modules have a single responsibility) ", + from: {}, + to: { + circular: true, + }, + }, + { + name: "no-orphans", + comment: + "This is an orphan module - it's likely not used (anymore?). Either use it or " + + "remove it. If it's logical this module is an orphan (i.e. it's a config file), " + + "add an exception for it in your dependency-cruiser configuration. By default " + + "this rule does not scrutinize dot-files (e.g. .eslintrc.js), TypeScript declaration " + + "files (.d.ts), tsconfig.json and some of the babel and webpack configs.", + severity: "warn", + from: { + orphan: true, + pathNot: [ + "(^|/)\\.[^/]+\\.(js|cjs|mjs|ts|json)$", + "\\.d\\.ts$", + "(^|/)tsconfig\\.json$", + "(^|/)(babel|webpack)\\.config\\.(js|cjs|mjs|ts|json)$", + "crashReporting\\.ts$", // Boilerplate file for future crash reporting setup + "utils/delay\\.ts$", // Utility function for delaying execution + ], + }, + to: {}, + }, + { + name: "no-deprecated-core", + comment: + "A module depends on a node core module that has been deprecated. Find an alternative - these are " + + "bound to exist - node doesn't deprecate lightly.", + severity: "warn", + from: {}, + to: { + dependencyTypes: ["core"], + path: ["^(punycode)$", "^(domain)$", "^(constants)$", "^(sys)$"], + }, + }, + { + name: "not-to-deprecated", + comment: + "This module uses a (version of an) npm module that has been deprecated. Either upgrade to a later " + + "version of that module, or find an alternative. Deprecated modules are a security risk.", + severity: "warn", + from: {}, + to: { + dependencyTypes: ["deprecated"], + }, + }, + { + name: "no-non-package-json", + severity: "error", + comment: + "This module depends on an npm package that isn't in the 'dependencies' section of your package.json. " + + "That's problematic as the package either (1) won't be available on live (2) will be " + + "available on live with an non-guaranteed version. Fix it by adding the package to the dependencies " + + "in your package.json.", + from: {}, + to: { + dependencyTypes: ["npm-no-pkg", "npm-unknown"], + }, + }, + { + name: "not-to-unresolvable", + comment: + "This module depends on a module that cannot be found ('resolved to disk'). If it's an npm " + + "module: add it to your package.json. In all other cases you likely already know what to do.", + severity: "error", + from: {}, + to: { + couldNotResolve: true, + }, + }, + { + name: "no-duplicate-dep-types", + comment: + "Likely this module depends on an external ('npm') package that occurs more than once " + + "in your package.json i.e. bot as a devDependencies and in dependencies. This will cause " + + "maintenance problems later on.", + severity: "warn", + from: {}, + to: { + moreThanOneDependencyType: true, + dependencyTypesNot: ["type-only"], + }, + }, + { + name: "not-to-spec", + comment: + "This module depends on a spec (test) file. The sole responsibility of a spec file is to test code. " + + "If there's something in a spec that's of use to other modules, it doesn't have that single " + + "responsibility anymore. Factor it out into (e.g.) a separate utility/ helper or a mock.", + severity: "error", + from: { + pathNot: "\\.(spec|test)\\.(js|mjs|cjs|ts|tsx)$", + }, + to: { + path: "\\.(spec|test)\\.(js|mjs|cjs|ts|tsx)$", + }, + }, + { + name: "not-to-dev-dep", + severity: "error", + comment: + "This module depends on an npm package from the 'devDependencies' section of your " + + "package.json. It looks like something that ships to production, though. To prevent problems " + + "with npm packages that aren't there on production declare it (only!) in the 'dependencies'" + + "section of your package.json. If this module is development only - add it to the " + + "from.pathNot re of the not-to-dev-dep rule in the dependency-cruiser configuration", + from: { + path: "^(app|src)", + pathNot: "\\.(spec|test)\\.(js|mjs|cjs|ts|tsx)$", + }, + to: { + dependencyTypes: ["npm-dev"], + pathNot: ["node_modules/@types/"], + exoticRequireNot: [ + "react-native/Libraries/Utilities/codegenNativeComponent", + "react-native/Libraries/Utilities/codegenNativeCommands", + ], + }, + }, + { + name: "optional-deps-used", + severity: "info", + comment: + "This module depends on an npm package that is declared as an optional dependency " + + "in your package.json. As this makes sense in limited situations only, it's flagged here. " + + "If you're using an optional dependency here by design - add an exception to your" + + "dependency-cruiser configuration.", + from: {}, + to: { + dependencyTypes: ["npm-optional"], + }, + }, + { + name: "peer-deps-used", + comment: + "This module depends on an npm package that is declared as a peer dependency " + + "in your package.json. This makes sense if your package is e.g. a plugin, but in " + + "other cases - maybe not so much. If the use of a peer dependency is intentional " + + "add an exception to your dependency-cruiser configuration.", + severity: "warn", + from: {}, + to: { + dependencyTypes: ["npm-peer"], + }, + }, + ], + options: { + doNotFollow: { + path: "node_modules", + }, + tsPreCompilationDeps: true, + tsConfig: { + fileName: "tsconfig.json", + }, + enhancedResolveOptions: { + exportsFields: ["exports"], + conditionNames: ["import", "require", "node", "default"], + // React Native / Metro bundler support for platform-specific extensions + // See: https://reactnative.dev/docs/platform-specific-code + // See: https://github.com/sverweij/dependency-cruiser/issues/511 + extensions, + }, + reporterOptions: { + dot: { + collapsePattern: "node_modules/(@[^/]+/[^/]+|[^/]+)", + }, + archi: { + collapsePattern: "^(app|src|test)/[^/]+", + }, + text: { + highlightFocused: true, + }, + }, + }, +} diff --git a/RN_TEMPLATE/.env b/RN_TEMPLATE/.env new file mode 100644 index 0000000..cea141c --- /dev/null +++ b/RN_TEMPLATE/.env @@ -0,0 +1,6 @@ +API_URL=https://auth.upay01.com +GOOGLE_WEB_CLIENT_ID=500211604129-4c5ij6e87jhlitoaclf8gfkco8ca6t9k.apps.googleusercontent.com +GOOGLE_IOS_CLIENT_ID=500211604129-uiln0fhiaj05jjg256oahkdcucpi6cqb.apps.googleusercontent.com +GOOGLE_ANDROID_CLIENT_ID=500211604129-f5rsg6e1bi7300i0goii8ckl9ld5jk3r.apps.googleusercontent.com +# iOS URL Scheme = reversed iOS Client ID (required for Google Sign-In callback) +GOOGLE_IOS_URL_SCHEME=com.googleusercontent.apps.500211604129-uiln0fhiaj05jjg256oahkdcucpi6cqb diff --git a/RN_TEMPLATE/.eslintignore b/RN_TEMPLATE/.eslintignore new file mode 100644 index 0000000..408b6c8 --- /dev/null +++ b/RN_TEMPLATE/.eslintignore @@ -0,0 +1,8 @@ +node_modules +ios +android +.expo +.vscode +ignite/ignite.json +package.json +.eslintignore diff --git a/RN_TEMPLATE/.eslintrc.js b/RN_TEMPLATE/.eslintrc.js new file mode 100644 index 0000000..2eeb1e8 --- /dev/null +++ b/RN_TEMPLATE/.eslintrc.js @@ -0,0 +1,105 @@ +// https://docs.expo.dev/guides/using-eslint/ +module.exports = { + root: true, + extends: [ + "plugin:@typescript-eslint/recommended", + "plugin:react/recommended", + "plugin:react-native/all", + // `expo` must come after `standard` or its globals configuration will be overridden + "expo", + // `jsx-runtime` must come after `expo` or it will be overridden + "plugin:react/jsx-runtime", + "prettier", + ], + plugins: ["reactotron", "prettier"], + rules: { + "prettier/prettier": "error", + // typescript-eslint + "@typescript-eslint/array-type": 0, + "@typescript-eslint/ban-ts-comment": 0, + "@typescript-eslint/no-explicit-any": 0, + "@typescript-eslint/no-unused-vars": [ + "error", + { + argsIgnorePattern: "^_", + varsIgnorePattern: "^_", + }, + ], + "@typescript-eslint/no-var-requires": 0, + "@typescript-eslint/no-require-imports": 0, + "@typescript-eslint/no-empty-object-type": 0, + // eslint + "no-use-before-define": 0, + "no-restricted-imports": [ + "error", + { + paths: [ + // Prefer named exports from 'react' instead of importing `React` + { + name: "react", + importNames: ["default"], + message: "Import named exports from 'react' instead.", + }, + { + name: "react-native", + importNames: ["SafeAreaView"], + message: "Use the SafeAreaView from 'react-native-safe-area-context' instead.", + }, + { + name: "react-native", + importNames: ["Text", "Button", "TextInput"], + message: "Use the custom wrapper component from '@/components'.", + }, + ], + }, + ], + // react + "react/prop-types": 0, + // react-native + "react-native/no-raw-text": 0, + // reactotron + "reactotron/no-tron-in-production": "error", + // eslint-config-standard overrides + "comma-dangle": 0, + "no-global-assign": 0, + "quotes": 0, + "space-before-function-paren": 0, + // eslint-import + "import/order": [ + "error", + { + "alphabetize": { + order: "asc", + caseInsensitive: true, + }, + "newlines-between": "always", + "groups": [["builtin", "external"], "internal", "unknown", ["parent", "sibling"], "index"], + "distinctGroup": false, + "pathGroups": [ + { + pattern: "react", + group: "external", + position: "before", + }, + { + pattern: "react-native", + group: "external", + position: "before", + }, + { + pattern: "expo{,-*}", + group: "external", + position: "before", + }, + { + pattern: "@/**", + group: "unknown", + position: "after", + }, + ], + "pathGroupsExcludedImportTypes": ["react", "react-native", "expo", "expo-*"], + }, + ], + "import/newline-after-import": 1, + }, +} diff --git a/RN_TEMPLATE/.gitignore b/RN_TEMPLATE/.gitignore new file mode 100644 index 0000000..12b98d6 --- /dev/null +++ b/RN_TEMPLATE/.gitignore @@ -0,0 +1,92 @@ +# OSX +# +.DS_Store + +# Xcode +# +build/ +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 +xcuserdata +*.xccheckout +*.moved-aside +DerivedData +*.hmap +*.ipa +*.xcuserstate +ios/.xcode.env.local + +# Android/IntelliJ +# +build/ +.idea +.gradle +local.properties +*.iml +*.hprof +.cxx/ + +# node.js +# +node_modules/ +npm-debug.log + +# BUCK +buck-out/ +\.buckd/ +*.keystore +!debug.keystore + +# fastlane +# +# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the +# screenshots whenever they are needed. +# For more information about the recommended setup visit: +# https://docs.fastlane.tools/best-practices/source-control/ + +**/fastlane/report.xml +**/fastlane/Preview.html +**/fastlane/screenshots +**/fastlane/test_output + +# Bundle artifact +*.jsbundle + +# Ignite-specific items below +# You can safely replace everything above this comment with whatever is +# in the default .gitignore generated by React-Native CLI + +# VS Code +.vscode + +# Expo +.expo/* +bin/Exponent.app +/android +/ios +expo-env.d.ts + +## Secrets +npm-debug.* +*.jks +*.p8 +*.p12 +*.key +*.mobileprovision +*.orig.* +web-build/ + +# Configurations +!env.js + +/coverage + +# Dependency Graph Visualizations +app-dependency-graph.svg +app-dependency-graph.png \ No newline at end of file diff --git a/RN_TEMPLATE/.maestro/flows/FavoritePodcast.yaml b/RN_TEMPLATE/.maestro/flows/FavoritePodcast.yaml new file mode 100644 index 0000000..506d0c8 --- /dev/null +++ b/RN_TEMPLATE/.maestro/flows/FavoritePodcast.yaml @@ -0,0 +1,37 @@ +# flow: run the login flow and then navigate to the demo podcast list screen, favorite a podcast, and then switch the list to only be favorites. + +appId: ${MAESTRO_APP_ID} +env: + FAVORITES_TEXT: "Switch on to only show favorites" # en.demoPodcastListScreen.accessibility.switch +onFlowStart: + - runFlow: ../shared/_OnFlowStart.yaml +--- +- runFlow: ../shared/_Login.yaml +- tapOn: "Podcast" +- assertVisible: "React Native Radio episodes" +- tapOn: + text: ${FAVORITES_TEXT} +- assertVisible: "This looks a bit empty" +- tapOn: + text: ${FAVORITES_TEXT} + # https://maestro.mobile.dev/troubleshooting/known-issues#android-accidental-double-tap + retryTapIfNoChange: false +- repeat: + times: 2 + commands: + - scroll +- copyTextFrom: + text: "RNR .*" # assumes all podcast titles start with RNR + index: 2 # grab the third one, others might not be fully visible +- longPressOn: ${maestro.copiedText} +- scrollUntilVisible: + element: + text: ${FAVORITES_TEXT} + direction: UP + timeout: 50000 + speed: 90 + visibilityPercentage: 100 +- tapOn: + text: ${FAVORITES_TEXT} +- assertVisible: ${maestro.copiedText} + diff --git a/RN_TEMPLATE/.maestro/flows/Login.yaml b/RN_TEMPLATE/.maestro/flows/Login.yaml new file mode 100644 index 0000000..f094ba1 --- /dev/null +++ b/RN_TEMPLATE/.maestro/flows/Login.yaml @@ -0,0 +1,13 @@ +#flow: Login +#intent: +# Open up our app and use the default credentials to login +# and navigate to the demo screen + +appId: ${MAESTRO_APP_ID} # the app id of the app we want to test +# You can find the appId of an Ignite app in the `app.json` file +# as the "package" under the "android" section and "bundleIdentifier" under the "ios" section +onFlowStart: + - runFlow: ../shared/_OnFlowStart.yaml +--- +- runFlow: ../shared/_Login.yaml + diff --git a/RN_TEMPLATE/.maestro/shared/_Login.yaml b/RN_TEMPLATE/.maestro/shared/_Login.yaml new file mode 100644 index 0000000..073eb58 --- /dev/null +++ b/RN_TEMPLATE/.maestro/shared/_Login.yaml @@ -0,0 +1,12 @@ +#flow: Shared _Login +#intent: shared login flow for any flow that needs to start with a log in. +appId: ${MAESTRO_APP_ID} +--- +- assertVisible: "Log In" +- tapOn: + text: "Tap to Log in!" +- assertVisible: "Your app, almost ready for launch!" +- tapOn: + text: "Let's go!" +- assertVisible: "Components to jump start your project!" + diff --git a/RN_TEMPLATE/.maestro/shared/_OnFlowStart.yaml b/RN_TEMPLATE/.maestro/shared/_OnFlowStart.yaml new file mode 100644 index 0000000..5393e74 --- /dev/null +++ b/RN_TEMPLATE/.maestro/shared/_OnFlowStart.yaml @@ -0,0 +1,39 @@ +# flow: Shared _OnFlowStart +#intent: +# launch the app with a completely clear state, wait for animations to settle, +# and click through the expo dev screens if needed. +# These conditionals slow the app launch down a little but are necessary because the expo +# dev server and launch screen are only shown when the new architecture is turned off in expo 53. +# So we check to see if we need to connect to the metro server... that loads the app and then we +# check if the dev menu is showing and dismiss it if necessary. +# Then the app is then launched and ready for the maestro tests to run. +# +# This flow should be included in every maestro test header as `onFlowStart` to ensure expo screens +# are bypassed if necessary. Example: +# +# appId: ${MAESTRO_APP_ID} +# onFlowStart: +# - runFlow: ../shared/_OnFlowStart.yaml +# --- +# [your maestro flow] +# +appId: ${MAESTRO_APP_ID} +--- +# launch the app with a clean slate +- launchApp: + clearState: true + clearKeychain: true + stopApp: true + +- waitForAnimationToEnd + +# conditionally run the dev client flow if the words "Development servers" is present. +# this uses the default maestro timeout and moves on if it doesn't see the text. +- runFlow: + when: + visible: 'Development servers' + commands: + # this regex allows for different hosts and ports + - tapOn: "http://.*:.*" + - waitForAnimationToEnd + - tapOn: "Close" # dismiss the bottom sheet diff --git a/RN_TEMPLATE/.npmrc b/RN_TEMPLATE/.npmrc new file mode 100644 index 0000000..8be2f68 --- /dev/null +++ b/RN_TEMPLATE/.npmrc @@ -0,0 +1,2 @@ +undefined +node-linker=hoisted diff --git a/RN_TEMPLATE/.prettierignore b/RN_TEMPLATE/.prettierignore new file mode 100644 index 0000000..ac37342 --- /dev/null +++ b/RN_TEMPLATE/.prettierignore @@ -0,0 +1,9 @@ +node_modules +ios +android +.expo +.vscode +ignite/ignite.json +package.json +.eslintignore +*.ejs diff --git a/RN_TEMPLATE/.prettierrc b/RN_TEMPLATE/.prettierrc new file mode 100644 index 0000000..3073d26 --- /dev/null +++ b/RN_TEMPLATE/.prettierrc @@ -0,0 +1,7 @@ +{ + "printWidth": 100, + "semi": false, + "singleQuote": false, + "trailingComma": "all", + "quoteProps": "consistent" +} \ No newline at end of file diff --git a/RN_TEMPLATE/CLAUDE.md b/RN_TEMPLATE/CLAUDE.md new file mode 100644 index 0000000..6e70926 --- /dev/null +++ b/RN_TEMPLATE/CLAUDE.md @@ -0,0 +1,314 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## user rools +每次开头请叫我哥哥 + +请严格遵守ui设计规范,使用响应式布局设计,不要硬编码 +请使用项目里面封装好的ui组件,如果没有请提醒我 +## Project Overview + +React Native + Expo mobile application built on **Ignite CLI boilerplate** (v11.4.0). Uses TypeScript, React Navigation 7, and NativeWind (Tailwind CSS). + +## Common Commands + +```bash +pnpm install # Install dependencies +pnpm run start # Start Expo dev client +pnpm run android # Run on Android (expo run:android) +pnpm run ios # Run on iOS (expo run:ios) +pnpm run compile # TypeScript type check (tsc --noEmit) +pnpm run lint # ESLint with auto-fix +pnpm run lint:check # ESLint without fix +pnpm run test # Run Jest tests +pnpm run test:watch # Run Jest in watch mode + +# EAS Builds (requires expo-cli) +pnpm run build:ios:sim # iOS simulator build +pnpm run build:android:sim # Android emulator build +pnpm run prebuild:clean # Regenerate native projects +``` + +## Architecture + +### Directory Structure + +- `app/` - Main application source + - `components/` - 12个基础 UI 组件(详见下方组件列表) + - `screens/` - Screen components connected to navigation + - `screens/DemoShowroomScreen/demos/` - 组件演示页面(展示组件用法,非实际组件) + - `navigators/` - React Navigation configuration (AppNavigator, DemoNavigator) + - `context/` - React Context providers (AuthContext, ThemeContext, ToastContext) + - `services/api/` - API layer using Apisauce (`authApi.ts` for auth, `index.ts` for base API) + - `theme/` - Design tokens (colors, spacing, typography) and theme system + - `i18n/` - Internationalization with i18next (8 languages: en, zh, ar, es, fr, hi, ja, ko) + - `utils/` - Utilities including MMKV storage wrapper + - `config/` - Environment-based configuration (dev/prod) + - `devtools/` - Reactotron configuration + +### Key Patterns + +**State Management**: React Context + MMKV for persistence. No Redux. + +**Theming**: Use `useAppTheme()` hook and `ThemedStyle` pattern: +```typescript +const $container: ThemedStyle = ({ colors, spacing }) => ({ + backgroundColor: colors.background, + padding: spacing.lg, +}) +``` + +**Navigation**: Two-layer structure - AppStack (auth/main screens) and DemoNavigator (bottom tabs). Type-safe routes defined in `navigationTypes.ts`. Auth screens: AuthWelcome, Login, Register, ForgotPassword. Main screens: Welcome, Profile, Settings, Security, Theme, Language, About, etc. + +**API Layer**: Singleton `Api` class in `services/api/index.ts` using Apisauce with typed responses. + +**Styling**: NativeWind (Tailwind CSS) configured. Use `className` prop for Tailwind styles. + +**Authentication**: `AuthContext` provides full auth flow with step-based state machines for login, register, and password reset. Supports email/password and Google Sign-In. Uses JWT tokens stored in MMKV. + +## UI 设计规范 + +### 参考文件 + +| 类型 | 文件路径 | 说明 | +|------|----------|------| +| **设计令牌** | `app/theme/colors.ts` | 颜色系统(浅色主题) | +| | `app/theme/colorsDark.ts` | 颜色系统(深色主题) | +| | `app/theme/spacing.ts` | 间距系统 | +| | `app/theme/typography.ts` | 字体排版 | +| | `app/theme/styles.ts` | 全局样式($styles.container 等) | +| **UI 参考页面** | `app/screens/DemoCommunityScreen.tsx` | **最佳参考** - 标准页面结构、ListItem 用法、Section 标题 | +| | `app/screens/WelcomeScreen.tsx` | useHeader hook 用法 | +| | `app/screens/ProfileScreen.tsx` | 设置页面、用户卡片 | +| **组件演示** | `app/screens/DemoShowroomScreen/demos/` | 所有组件的用法示例(类似 Storybook) | + +### 设计令牌 + +**颜色语义**(定义在 `app/theme/colors.ts`): +```typescript +text: palette.neutral800 // 主要文本 +textDim: palette.neutral600 // 次要文本 +background: palette.neutral200 // 屏幕背景 +border: palette.neutral400 // 边框 +tint: palette.primary500 // 主色调(按钮、链接) +separator: palette.neutral300 // 分隔线 +error: palette.angry500 // 错误 +``` + +**间距系统**(`app/theme/spacing.ts`): +```typescript +xxxs: 2, xxs: 4, xs: 8, sm: 12, md: 16, lg: 24, xl: 32, xxl: 48, xxxl: 64 +``` + +**字体大小**(`app/components/Text.tsx`): +```typescript +xxl: 36, xl: 24, lg: 20, md: 18, sm: 16(默认), xs: 14, xxs: 12 +``` + +### Header 使用规范 + +**必须使用 `useHeader` hook**,不要直接在 Screen 内部放置 `
` 组件。 + +```typescript +// ✅ 正确写法 +import { useHeader } from "@/utils/useHeader" + +useHeader({ + title: translate("screenName:title"), + leftIcon: "back", + onLeftPress: () => navigation.goBack(), +}, []) + +return ( + + {/* 内容 */} + +) + +const $container: ThemedStyle = ({ spacing }) => ({ + paddingTop: spacing.lg, // 覆盖 $styles.container 的 56px +}) +``` + +**原因**: Header 组件默认添加顶部安全区域 padding,直接放在 Screen 内容中会导致 Header 上方出现额外空白。`useHeader` 通过 `navigation.setOptions()` 将 Header 设置为导航层的一部分。 + +### 容器间距规范 + +`$styles.container`(定义在 `app/theme/styles.ts`): +```typescript +container: { + paddingTop: spacing.lg + spacing.xl, // = 56px(为无 Header 的 Tab 页面设计) + paddingHorizontal: spacing.lg, // = 24px +} +``` + +**使用 useHeader 的页面**需在 `$container` 中覆盖 paddingTop 为 `spacing.lg` (24px)。 + +**不要**在页面内容中再添加额外的 paddingTop 或 marginTop,避免重复间距。 + +### ListItem 使用规范 + +**参考**: `app/screens/DemoCommunityScreen.tsx` + +**默认无分隔线**,保持简洁风格: +```typescript + +``` + +只有**连续列表项**需要视觉分隔时,才使用 `bottomSeparator`,且**最后一项不需要**: +```typescript + + + +``` + +### Section 标题规范 + +**参考**: `app/screens/DemoCommunityScreen.tsx` 的 `$sectionTitle` 样式 + +使用 `preset="subheading"` 作为 section 标题: +```typescript + + +const $sectionTitle: ThemedStyle = ({ spacing }) => ({ + marginTop: spacing.xxl, // 48px +}) +``` + +### 页面结构模板 + +**有 Header 的页面**(如设置、详情页): +```typescript +import { useHeader } from "@/utils/useHeader" + +export const ExampleScreen: FC = ({ navigation }) => { + const { themed } = useAppTheme() + + useHeader({ + title: translate("exampleScreen:title"), + leftIcon: "back", + onLeftPress: () => navigation.goBack(), + }, []) + + return ( + + {/* 内容 */} + + ) +} + +const $container: ThemedStyle = ({ spacing }) => ({ + paddingTop: spacing.lg, +}) +``` + +**无 Header 的 Tab 页面**(参考 `DemoCommunityScreen.tsx`): +```typescript +export const ExampleTabScreen: FC = () => { + const { themed } = useAppTheme() + + return ( + + + + + + + + + ) +} + +const $title: ThemedStyle = ({ spacing }) => ({ + marginBottom: spacing.sm, +}) + +const $description: ThemedStyle = ({ spacing }) => ({ + marginBottom: spacing.lg, +}) + +const $sectionTitle: ThemedStyle = ({ spacing }) => ({ + marginTop: spacing.xxl, +}) +``` + +### 组件列表 (`app/components/`) + +| 组件 | 用途 | +|------|------| +| `Screen.tsx` | 屏幕容器(SafeArea + 键盘适配 + 滚动) | +| `Header.tsx` | 页面头部导航栏 | +| `Card.tsx` | 卡片容器 | +| `TextField.tsx` | 文本输入框 | +| `Button.tsx` | 按钮(default/filled/reversed 预设) | +| `EmptyState.tsx` | 空状态占位 | +| `ListItem.tsx` | 列表项 | +| `Icon.tsx` | 图标 | +| `Text.tsx` | 文本(heading/subheading/bold 等预设) | +| `AutoImage.tsx` | 自适应图片 | +| `Toggle/` | 开关/复选框/单选框 | +| `Avatar.tsx` | 用户头像(使用 expo-image 缓存) | +| `Modal.tsx` | 模态框(bottom/center 预设) | +| `Dialog.tsx` | 对话框(确认/提示) | + +**注意**: `screens/DemoShowroomScreen/demos/` 目录下的 `Demo*.tsx` 文件是组件**演示页面**,用于展示组件的各种用法和变体,类似 Storybook,不是实际组件。 + +### 自适应布局 + +**Screen 组件预设**: +- `preset="fixed"` - 固定布局,不可滚动 +- `preset="scroll"` - 始终可滚动 +- `preset="auto"` - 智能判断:内容超出屏幕时才启用滚动 + +**安全区域**: 使用 `useSafeAreaInsetsStyle` hook 处理刘海屏 +```typescript + + {/* 内容自动避开刘海和底部指示条 */} + +``` + +**间距系统** (`app/theme/spacing.ts`): +```typescript +xxxs: 2, xxs: 4, xs: 8, sm: 12, md: 16, lg: 24, xl: 32, xxl: 48, xxxl: 64 +``` + +**字体大小** (`app/components/Text.tsx`) - 固定像素值,无响应式: +```typescript +xxl: 36, xl: 24, lg: 20, md: 18, sm: 16(默认), xs: 14, xxs: 12 +``` + +### Path Aliases + +- `@/*` → `app/*` +- `@assets/*` → `assets/*` + +## Import Restrictions (ESLint enforced) + +- Use `Text`, `Button`, `TextInput` from `@/components`, not from `react-native` +- Use `SafeAreaView` from `react-native-safe-area-context`, not from `react-native` +- Import named exports from `react` (not `import React from 'react'`) + +## Testing + +Jest with Testing Library for React Native. Test files use `.test.ts(x)` suffix. Setup in `test/setup.ts`. + +Run single test: +```bash +pnpm run test -- --testPathPattern="storage" +``` + +## Web Support + +This app runs on web via Expo. Use `pnpm run start` and press `w` to open web. Note: React Navigation's `headerRight` may not work reliably on web - consider placing header elements directly inside Screen components if targeting web. + +## Running on Physical Devices diff --git a/RN_TEMPLATE/README.md b/RN_TEMPLATE/README.md new file mode 100644 index 0000000..1155dde --- /dev/null +++ b/RN_TEMPLATE/README.md @@ -0,0 +1,77 @@ +# Welcome to your new ignited app! + +> The latest and greatest boilerplate for Infinite Red opinions + +This is the boilerplate that [Infinite Red](https://infinite.red) uses as a way to test bleeding-edge changes to our React Native stack. + +- [Quick start documentation](https://github.com/infinitered/ignite/blob/master/docs/boilerplate/Boilerplate.md) +- [Full documentation](https://github.com/infinitered/ignite/blob/master/docs/README.md) + +## Getting Started + +```bash +pnpm install +pnpm run start +``` + +To make things work on your local simulator, or on your phone, you need first to [run `eas build`](https://github.com/infinitered/ignite/blob/master/docs/expo/EAS.md). We have many shortcuts on `package.json` to make it easier: + +```bash +pnpm run build:ios:sim # build for ios simulator +pnpm run build:ios:device # build for ios device +pnpm run build:ios:prod # build for ios device +``` + +### `./assets` + +This directory is designed to organize and store various assets, making it easy for you to manage and use them in your application. The assets are further categorized into subdirectories, including `icons` and `images`: + +```tree +assets +├── icons +└── images +``` + +**icons** +This is where your icon assets will live. These icons can be used for buttons, navigation elements, or any other UI components. The recommended format for icons is PNG, but other formats can be used as well. + +Ignite comes with a built-in `Icon` component. You can find detailed usage instructions in the [docs](https://github.com/infinitered/ignite/blob/master/docs/boilerplate/app/components/Icon.md). + +**images** +This is where your images will live, such as background images, logos, or any other graphics. You can use various formats such as PNG, JPEG, or GIF for your images. + +Another valuable built-in component within Ignite is the `AutoImage` component. You can find detailed usage instructions in the [docs](https://github.com/infinitered/ignite/blob/master/docs/Components-AutoImage.md). + +How to use your `icon` or `image` assets: + +```typescript +import { Image } from 'react-native'; + +const MyComponent = () => { + return ( + + ); +}; +``` + +## Running Maestro end-to-end tests + +Follow our [Maestro Setup](https://ignitecookbook.com/docs/recipes/MaestroSetup) recipe. + +## Next Steps + +### Ignite Cookbook + +[Ignite Cookbook](https://ignitecookbook.com/) is an easy way for developers to browse and share code snippets (or “recipes”) that actually work. + +### Upgrade Ignite boilerplate + +Read our [Upgrade Guide](https://ignitecookbook.com/docs/recipes/UpdatingIgnite) to learn how to upgrade your Ignite project. + +## Community + +⭐️ Help us out by [starring on GitHub](https://github.com/infinitered/ignite), filing bug reports in [issues](https://github.com/infinitered/ignite/issues) or [ask questions](https://github.com/infinitered/ignite/discussions). + +💬 Join us on [Slack](https://join.slack.com/t/infiniteredcommunity/shared_invite/zt-1f137np4h-zPTq_CbaRFUOR_glUFs2UA) to discuss. + +📰 Make our Editor-in-chief happy by [reading the React Native Newsletter](https://reactnativenewsletter.com/). diff --git a/RN_TEMPLATE/app.config.ts b/RN_TEMPLATE/app.config.ts new file mode 100644 index 0000000..c421eeb --- /dev/null +++ b/RN_TEMPLATE/app.config.ts @@ -0,0 +1,50 @@ +import { ExpoConfig, ConfigContext } from "@expo/config" + +/** + * Use tsx/cjs here so we can use TypeScript for our Config Plugins + * and not have to compile them to JavaScript. + * + * See https://docs.expo.dev/config-plugins/plugins/#add-typescript-support-and-convert-to-dynamic-app-config + */ +import "tsx/cjs" + +/** + * @param config ExpoConfig coming from the static config app.json if it exists + * + * You can read more about Expo's Configuration Resolution Rules here: + * https://docs.expo.dev/workflow/configuration/#configuration-resolution-rules + */ +module.exports = ({ config }: ConfigContext): Partial => { + const existingPlugins = config.plugins ?? [] + + return { + ...config, + ios: { + ...config.ios, + // This privacyManifests is to get you started. + // See Expo's guide on apple privacy manifests here: + // https://docs.expo.dev/guides/apple-privacy/ + // You may need to add more privacy manifests depending on your app's usage of APIs. + // More details and a list of "required reason" APIs can be found in the Apple Developer Documentation. + // https://developer.apple.com/documentation/bundleresources/privacy-manifest-files + privacyManifests: { + NSPrivacyAccessedAPITypes: [ + { + NSPrivacyAccessedAPIType: "NSPrivacyAccessedAPICategoryUserDefaults", + NSPrivacyAccessedAPITypeReasons: ["CA92.1"], // CA92.1 = "Access info from same app, per documentation" + }, + ], + }, + }, + plugins: [ + ...existingPlugins, + [ + "@react-native-google-signin/google-signin", + { + // iOS URL Scheme = reversed iOS Client ID (for Google Sign-In callback) + iosUrlScheme: "com.googleusercontent.apps.500211604129-uiln0fhiaj05jjg256oahkdcucpi6cqb", + }, + ], + ], + } +} diff --git a/RN_TEMPLATE/app.json b/RN_TEMPLATE/app.json new file mode 100644 index 0000000..4e02295 --- /dev/null +++ b/RN_TEMPLATE/app.json @@ -0,0 +1,71 @@ +{ + "name": "upay", + "slug": "test", + "scheme": "upay", + "version": "1.0.0", + "orientation": "portrait", + "userInterfaceStyle": "automatic", + "icon": "./assets/images/app-icon-all.png", + "updates": { + "fallbackToCacheTimeout": 0 + }, + "newArchEnabled": true, + "jsEngine": "hermes", + "assetBundlePatterns": [ + "**/*" + ], + "android": { + "icon": "./assets/images/app-icon-android-legacy.png", + "package": "com.upay", + "adaptiveIcon": { + "foregroundImage": "./assets/images/app-icon-android-adaptive-foreground.png", + "backgroundImage": "./assets/images/app-icon-android-adaptive-background.png" + }, + "allowBackup": false, + "edgeToEdgeEnabled": true + }, + "ios": { + "icon": "./assets/images/app-icon-ios.png", + "supportsTablet": true, + "bundleIdentifier": "com.upay" + }, + "web": { + "favicon": "./assets/images/app-icon-web-favicon.png", + "bundler": "metro" + }, + "plugins": [ + "expo-localization", + "expo-font", + [ + "expo-splash-screen", + { + "image": "./assets/images/app-icon-android-adaptive-foreground.png", + "imageWidth": 300, + "resizeMode": "contain", + "backgroundColor": "#191015" + } + ], + [ + "react-native-edge-to-edge", + { + "android": { + "parentTheme": "Light", + "enforceNavigationBarContrast": false + } + } + ], + "expo-build-properties" + ], + "experiments": { + "tsconfigPaths": true + }, + "extra": { + "ignite": { + "version": "11.4.0" + }, + "eas": { + "projectId": "03377418-ceb0-4ec9-8dce-7605150d5225" + } + }, + "owner": "sofio" +} diff --git a/RN_TEMPLATE/app/app.tsx b/RN_TEMPLATE/app/app.tsx new file mode 100644 index 0000000..a7ceb25 --- /dev/null +++ b/RN_TEMPLATE/app/app.tsx @@ -0,0 +1,117 @@ +/* eslint-disable import/first */ +/** + * Welcome to the main entry point of the app. In this file, we'll + * be kicking off our app. + * + * Most of this file is boilerplate and you shouldn't need to modify + * it very often. But take some time to look through and understand + * what is going on here. + * + * The app navigation resides in ./app/navigators, so head over there + * if you're interested in adding screens and navigators. + */ +if (__DEV__) { + // Load Reactotron in development only. + // Note that you must be using metro's `inlineRequires` for this to work. + // If you turn it off in metro.config.js, you'll have to manually import it. + require("./devtools/ReactotronConfig.ts") +} +import "./utils/gestureHandler" + +import { useEffect, useState } from "react" +import { useFonts } from "expo-font" +import * as Linking from "expo-linking" +import { GestureHandlerRootView } from "react-native-gesture-handler" +import { KeyboardProvider } from "react-native-keyboard-controller" +import { initialWindowMetrics, SafeAreaProvider } from "react-native-safe-area-context" + +import { AuthProvider } from "./context/AuthContext" +import { ToastProvider } from "./context/ToastContext" +import { initI18n } from "./i18n" +import { AppNavigator } from "./navigators/AppNavigator" +import { useNavigationPersistence } from "./navigators/navigationUtilities" +import { ThemeProvider } from "./theme/context" +import { customFontsToLoad } from "./theme/typography" +import { loadDateFnsLocale } from "./utils/formatDate" +import * as storage from "./utils/storage" + +export const NAVIGATION_PERSISTENCE_KEY = "NAVIGATION_STATE" + +// Web linking configuration +const prefix = Linking.createURL("/") +const config = { + screens: { + Login: { + path: "", + }, + Welcome: "welcome", + Demo: { + screens: { + DemoShowroom: { + path: "showroom/:queryIndex?/:itemIndex?", + }, + DemoDebug: "debug", + DemoPodcastList: "podcast", + DemoCommunity: "community", + }, + }, + }, +} + +/** + * This is the root component of our app. + * @param {AppProps} props - The props for the `App` component. + * @returns {JSX.Element} The rendered `App` component. + */ +export function App() { + const { + initialNavigationState, + onNavigationStateChange, + isRestored: isNavigationStateRestored, + } = useNavigationPersistence(storage, NAVIGATION_PERSISTENCE_KEY) + + const [areFontsLoaded, fontLoadError] = useFonts(customFontsToLoad) + const [isI18nInitialized, setIsI18nInitialized] = useState(false) + + useEffect(() => { + initI18n() + .then(() => setIsI18nInitialized(true)) + .then(() => loadDateFnsLocale()) + }, []) + + // Before we show the app, we have to wait for our state to be ready. + // In the meantime, don't render anything. This will be the background + // color set in native by rootView's background color. + // In iOS: application:didFinishLaunchingWithOptions: + // In Android: https://stackoverflow.com/a/45838109/204044 + // You can replace with your own loading component if you wish. + if (!isNavigationStateRestored || !isI18nInitialized || (!areFontsLoaded && !fontLoadError)) { + return null + } + + const linking = { + prefixes: [prefix], + config, + } + + // otherwise, we're ready to render the app + return ( + + + + + + + + + + + + + + ) +} diff --git a/RN_TEMPLATE/app/components/AutoImage.tsx b/RN_TEMPLATE/app/components/AutoImage.tsx new file mode 100644 index 0000000..d4a13ed --- /dev/null +++ b/RN_TEMPLATE/app/components/AutoImage.tsx @@ -0,0 +1,89 @@ +import { useLayoutEffect, useState } from "react" +import { Image, ImageProps, ImageURISource, Platform, PixelRatio } from "react-native" + +export interface AutoImageProps extends ImageProps { + /** + * How wide should the image be? + */ + maxWidth?: number + /** + * How tall should the image be? + */ + maxHeight?: number + headers?: { + [key: string]: string + } +} + +/** + * A hook that will return the scaled dimensions of an image based on the + * provided dimensions' aspect ratio. If no desired dimensions are provided, + * it will return the original dimensions of the remote image. + * + * How is this different from `resizeMode: 'contain'`? Firstly, you can + * specify only one side's size (not both). Secondly, the image will scale to fit + * the desired dimensions instead of just being contained within its image-container. + * @param {number} remoteUri - The URI of the remote image. + * @param {number} dimensions - The desired dimensions of the image. If not provided, the original dimensions will be returned. + * @returns {[number, number]} - The scaled dimensions of the image. + */ +export function useAutoImage( + remoteUri: string, + headers?: { + [key: string]: string + }, + dimensions?: [maxWidth?: number, maxHeight?: number], +): [width: number, height: number] { + const [[remoteWidth, remoteHeight], setRemoteImageDimensions] = useState([0, 0]) + const remoteAspectRatio = remoteWidth / remoteHeight + const [maxWidth, maxHeight] = dimensions ?? [] + + useLayoutEffect(() => { + if (!remoteUri) return + + if (!headers) { + Image.getSize(remoteUri, (w, h) => setRemoteImageDimensions([w, h])) + } else { + Image.getSizeWithHeaders(remoteUri, headers, (w, h) => setRemoteImageDimensions([w, h])) + } + }, [remoteUri, headers]) + + if (Number.isNaN(remoteAspectRatio)) return [0, 0] + + if (maxWidth && maxHeight) { + const aspectRatio = Math.min(maxWidth / remoteWidth, maxHeight / remoteHeight) + return [ + PixelRatio.roundToNearestPixel(remoteWidth * aspectRatio), + PixelRatio.roundToNearestPixel(remoteHeight * aspectRatio), + ] + } else if (maxWidth) { + return [maxWidth, PixelRatio.roundToNearestPixel(maxWidth / remoteAspectRatio)] + } else if (maxHeight) { + return [PixelRatio.roundToNearestPixel(maxHeight * remoteAspectRatio), maxHeight] + } else { + return [remoteWidth, remoteHeight] + } +} + +/** + * An Image component that automatically sizes a remote or data-uri image. + * @see [Documentation and Examples]{@link https://docs.infinite.red/ignite-cli/boilerplate/app/components/AutoImage/} + * @param {AutoImageProps} props - The props for the `AutoImage` component. + * @returns {JSX.Element} The rendered `AutoImage` component. + */ +export function AutoImage(props: AutoImageProps) { + const { maxWidth, maxHeight, ...ImageProps } = props + const source = props.source as ImageURISource + const headers = source?.headers + + const [width, height] = useAutoImage( + Platform.select({ + web: (source?.uri as string) ?? (source as string), + default: source?.uri as string, + }), + headers, + [maxWidth, maxHeight], + ) + + return +} diff --git a/RN_TEMPLATE/app/components/Avatar.tsx b/RN_TEMPLATE/app/components/Avatar.tsx new file mode 100644 index 0000000..5160dc9 --- /dev/null +++ b/RN_TEMPLATE/app/components/Avatar.tsx @@ -0,0 +1,71 @@ +import { StyleProp, TextStyle, View, ViewStyle } from "react-native" +import { Image, ImageStyle } from "expo-image" + +import { useAppTheme } from "@/theme/context" +import type { ThemedStyle } from "@/theme/types" +import { s } from "@/utils/responsive" + +import { Text } from "./Text" + +export interface AvatarProps { + /** + * The image URI to display + */ + uri?: string | null + /** + * Fallback text to display when no image (usually first letter of name) + */ + fallback?: string + /** + * Size of the avatar (width and height) + * @default 80 + */ + size?: number + /** + * Optional style override for the container + */ + style?: StyleProp +} + +/** + * A reusable Avatar component with expo-image for efficient caching. + * Displays an image or a fallback letter when no image is available. + */ +export function Avatar(props: AvatarProps) { + const { uri, fallback = "U", size = s(80), style } = props + const { themed, theme } = useAppTheme() + + const borderRadius = size / 2 + const fontSize = size * 0.4 + + if (uri) { + return ( + + ) + } + + return ( + + + {fallback.charAt(0).toUpperCase()} + + + ) +} + +const $image: ImageStyle = { + backgroundColor: "#e1e1e1", +} + +const $placeholder: ThemedStyle = ({ colors }) => ({ + backgroundColor: colors.tint, + justifyContent: "center", + alignItems: "center", +}) diff --git a/RN_TEMPLATE/app/components/Button.tsx b/RN_TEMPLATE/app/components/Button.tsx new file mode 100644 index 0000000..8f7e2f8 --- /dev/null +++ b/RN_TEMPLATE/app/components/Button.tsx @@ -0,0 +1,349 @@ +import { ComponentType, useEffect, useRef } from "react" +import { + Animated, + Pressable, + PressableProps, + PressableStateCallbackType, + StyleProp, + TextStyle, + View, + ViewStyle, +} from "react-native" + +import { useAppTheme } from "@/theme/context" +import { $styles } from "@/theme/styles" +import type { ThemedStyle, ThemedStyleArray } from "@/theme/types" +import { s, fs } from "@/utils/responsive" + +import { Text, TextProps } from "./Text" + +/** + * Three-dot loading animation component + */ +function LoadingDots({ color }: { color: string }) { + const dot1 = useRef(new Animated.Value(0)).current + const dot2 = useRef(new Animated.Value(0)).current + const dot3 = useRef(new Animated.Value(0)).current + + useEffect(() => { + const animateDot = (dot: Animated.Value, delay: number) => { + return Animated.loop( + Animated.sequence([ + Animated.delay(delay), + Animated.timing(dot, { + toValue: 1, + duration: 300, + useNativeDriver: true, + }), + Animated.timing(dot, { + toValue: 0, + duration: 300, + useNativeDriver: true, + }), + ]), + ) + } + + const animation = Animated.parallel([ + animateDot(dot1, 0), + animateDot(dot2, 150), + animateDot(dot3, 300), + ]) + + animation.start() + + return () => animation.stop() + }, [dot1, dot2, dot3]) + + const dotStyle = (animatedValue: Animated.Value) => ({ + width: s(8), + height: s(8), + borderRadius: s(4), + backgroundColor: color, + marginHorizontal: 3, + opacity: animatedValue.interpolate({ + inputRange: [0, 1], + outputRange: [0.3, 1], + }), + transform: [ + { + scale: animatedValue.interpolate({ + inputRange: [0, 1], + outputRange: [0.8, 1.2], + }), + }, + ], + }) + + return ( + + + + + + ) +} + +type Presets = "default" | "filled" | "reversed" | "link" + +export interface ButtonAccessoryProps { + style: StyleProp + pressableState: PressableStateCallbackType + disabled?: boolean +} + +export interface ButtonProps extends PressableProps { + /** + * Text which is looked up via i18n. + */ + tx?: TextProps["tx"] + /** + * The text to display if not using `tx` or nested components. + */ + text?: TextProps["text"] + /** + * Optional options to pass to i18n. Useful for interpolation + * as well as explicitly setting locale or translation fallbacks. + */ + txOptions?: TextProps["txOptions"] + /** + * An optional style override useful for padding & margin. + */ + style?: StyleProp + /** + * An optional style override for the "pressed" state. + */ + pressedStyle?: StyleProp + /** + * An optional style override for the button text. + */ + textStyle?: StyleProp + /** + * An optional style override for the button text when in the "pressed" state. + */ + pressedTextStyle?: StyleProp + /** + * An optional style override for the button text when in the "disabled" state. + */ + disabledTextStyle?: StyleProp + /** + * One of the different types of button presets. + */ + preset?: Presets + /** + * An optional component to render on the right side of the text. + * Example: `RightAccessory={(props) => }` + */ + RightAccessory?: ComponentType + /** + * An optional component to render on the left side of the text. + * Example: `LeftAccessory={(props) => }` + */ + LeftAccessory?: ComponentType + /** + * Children components. + */ + children?: React.ReactNode + /** + * disabled prop, accessed directly for declarative styling reasons. + * https://reactnative.dev/docs/pressable#disabled + */ + disabled?: boolean + /** + * An optional style override for the disabled state + */ + disabledStyle?: StyleProp + /** + * Show loading state with animated dots + */ + loading?: boolean +} + +/** + * A component that allows users to take actions and make choices. + * Wraps the Text component with a Pressable component. + * @see [Documentation and Examples]{@link https://docs.infinite.red/ignite-cli/boilerplate/app/components/Button/} + * @param {ButtonProps} props - The props for the `Button` component. + * @returns {JSX.Element} The rendered `Button` component. + * @example + * + + + + + + + + + + + , + + + + + + + + + + + + + + + + , + + + + + + + + + + + + + , + + + + + + + + + + + + + + + + , + ], +} diff --git a/RN_TEMPLATE/app/screens/ShowroomScreen/demos/DemoCard.tsx b/RN_TEMPLATE/app/screens/ShowroomScreen/demos/DemoCard.tsx new file mode 100644 index 0000000..8b47f9f --- /dev/null +++ b/RN_TEMPLATE/app/screens/ShowroomScreen/demos/DemoCard.tsx @@ -0,0 +1,180 @@ +/* eslint-disable react/jsx-key, react-native/no-inline-styles */ +import { AutoImage } from "@/components/AutoImage" +import { Button } from "@/components/Button" +import { Card } from "@/components/Card" +import { Icon } from "@/components/Icon" +import { s } from "@/utils/responsive" + +import { DemoDivider } from "../DemoDivider" +import { Demo } from "./types" +import { DemoUseCase } from "../DemoUseCase" + +export const DemoCard: Demo = { + name: "Card", + description: "demoCard:description", + data: ({ theme }) => [ + + + + + , + + + + + + + + + + , + + + + + + , + + + } + /> + } + ContentComponent={ +