@fadyshawky/react-native-magic 2.2.0 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +90 -55
  3. package/index.js +4 -0
  4. package/package.json +9 -3
  5. package/scripts/askPackageName.js +10 -5
  6. package/template/.env.development +8 -6
  7. package/template/.env.example +15 -5
  8. package/template/.env.production +8 -6
  9. package/template/.env.staging +8 -6
  10. package/template/.eslintrc.js +14 -0
  11. package/template/.husky/pre-commit +1 -0
  12. package/template/App.tsx +47 -16
  13. package/template/__tests__/App.test.tsx +28 -10
  14. package/template/babel.config.js +20 -1
  15. package/template/docs/ARCHITECTURE.md +40 -10
  16. package/template/docs/BEST_PRACTICES.md +10 -1
  17. package/template/docs/CUSTOMIZATION.md +118 -5
  18. package/template/docs/design-system.html +1164 -0
  19. package/template/docs/wireframes.html +411 -0
  20. package/template/index.js +10 -0
  21. package/template/jest.config.js +16 -1
  22. package/template/jest.setup.js +61 -0
  23. package/template/package-lock.json +12178 -8293
  24. package/template/package.json +53 -19
  25. package/template/react-native.config.js +3 -0
  26. package/template/resources/fonts/.gitkeep +0 -0
  27. package/template/scripts/ci-sync-env.cjs +71 -0
  28. package/template/src/assets/brand/logo-mark.svg +8 -0
  29. package/template/src/assets/brand/logo-mono.svg +9 -0
  30. package/template/src/assets/brand/logo-primary.svg +15 -0
  31. package/template/src/assets/brand/wordmark-dark.svg +18 -0
  32. package/template/src/common/components/AppBottomSheet.tsx +87 -0
  33. package/template/src/common/components/AppSwitch.tsx +75 -0
  34. package/template/src/common/components/AppTextInput.tsx +161 -0
  35. package/template/src/common/components/Avatar.tsx +75 -0
  36. package/template/src/common/components/Badge.tsx +66 -0
  37. package/template/src/common/components/CardScroller.tsx +58 -0
  38. package/template/src/common/components/Cards.tsx +13 -7
  39. package/template/src/common/components/Carousel.tsx +196 -0
  40. package/template/src/common/components/Checkbox.tsx +85 -0
  41. package/template/src/common/components/Chip.tsx +55 -0
  42. package/template/src/common/components/Dropdown.tsx +202 -0
  43. package/template/src/common/components/ErrorBoundary.tsx +82 -0
  44. package/template/src/common/components/FlatListWrapper.tsx +8 -8
  45. package/template/src/common/components/ListItem.tsx +90 -0
  46. package/template/src/common/components/LoadingComponent.tsx +8 -2
  47. package/template/src/common/components/Logo.tsx +77 -0
  48. package/template/src/common/components/ModalDialog.tsx +141 -0
  49. package/template/src/common/components/NetworkBanner.tsx +47 -0
  50. package/template/src/common/components/OTPInput.tsx +0 -1
  51. package/template/src/common/components/PrimaryButton.tsx +0 -14
  52. package/template/src/common/components/PrimaryTextInput.tsx +66 -130
  53. package/template/src/common/components/RadioGroup.tsx +95 -0
  54. package/template/src/common/components/SafeText.tsx +4 -3
  55. package/template/src/common/components/SearchBar.tsx +7 -5
  56. package/template/src/common/components/SegmentedControl.tsx +77 -0
  57. package/template/src/common/components/Skeleton.tsx +47 -0
  58. package/template/src/common/components/TryAgain.tsx +4 -2
  59. package/template/src/common/helpers/arrayHelpers.ts +2 -2
  60. package/template/src/common/helpers/defaultKeyIdExtractor.ts +1 -1
  61. package/template/src/common/helpers/regexHelpers.ts +1 -2
  62. package/template/src/common/helpers/stringsHelpers.ts +0 -1
  63. package/template/src/common/hooks/useBackHandler.ts +5 -2
  64. package/template/src/common/hooks/useEventRegister.ts +1 -1
  65. package/template/src/common/hooks/useFlatListActions.ts +1 -1
  66. package/template/src/common/hooks/useWhyDidYouUpdate.ts +1 -1
  67. package/template/src/common/localization/LocalizationProvider.tsx +1 -1
  68. package/template/src/common/localization/RTLInitializer.tsx +1 -1
  69. package/template/src/common/localization/dateFormatter.ts +0 -1
  70. package/template/src/common/localization/intlFormatter.ts +0 -1
  71. package/template/src/common/localization/localization.ts +2 -2
  72. package/template/src/common/localization/translations/homeLocalization.ts +14 -0
  73. package/template/src/common/localization/translations/loginLocalization.ts +8 -0
  74. package/template/src/common/localization/translations/mainNavigationLocalization.ts +2 -0
  75. package/template/src/common/localization/translations/profileLocalization.ts +16 -0
  76. package/template/src/common/utils/index.tsx +0 -6
  77. package/template/src/common/validations/commonValidations.ts +2 -2
  78. package/template/src/core/api/errorHandler.ts +1 -1
  79. package/template/src/core/api/responseHandlers.ts +1 -3
  80. package/template/src/core/api/serverHeaders.ts +61 -12
  81. package/template/src/core/notifications/notificationAuth.ts +6 -0
  82. package/template/src/core/notifications/notificationService.ts +125 -0
  83. package/template/src/core/notifications/routeFromNotificationData.ts +32 -0
  84. package/template/src/core/store/categories/categoriesActions.ts +25 -0
  85. package/template/src/core/store/categories/categoriesSlice.ts +51 -0
  86. package/template/src/core/store/categories/categoriesState.ts +19 -0
  87. package/template/src/core/store/rootReducer.ts +2 -0
  88. package/template/src/core/store/store.tsx +6 -1
  89. package/template/src/core/store/user/userActions.ts +75 -14
  90. package/template/src/core/store/user/userSlice.ts +49 -26
  91. package/template/src/core/store/user/userState.ts +6 -4
  92. package/template/src/core/theme/ThemeProvider.tsx +5 -3
  93. package/template/src/core/theme/brand.ts +50 -0
  94. package/template/src/core/theme/colors.ts +113 -99
  95. package/template/src/core/theme/commonConsts.ts +2 -2
  96. package/template/src/core/theme/commonStyles.ts +1 -1
  97. package/template/src/core/theme/themes.ts +2 -0
  98. package/template/src/core/theme/types.ts +4 -2
  99. package/template/src/core/utils/stringUtils.ts +1 -1
  100. package/template/src/design-system/index.ts +2 -0
  101. package/template/src/design-system/tokens/brand.ts +6 -0
  102. package/template/src/design-system/tokens/index.ts +3 -0
  103. package/template/src/design-system/tokens/palette.ts +4 -0
  104. package/template/src/design-system/tokens/typography-spacing.ts +2 -0
  105. package/template/src/navigation/AuthStack.tsx +1 -4
  106. package/template/src/navigation/HeaderComponents.tsx +6 -3
  107. package/template/src/navigation/MainStack.tsx +18 -6
  108. package/template/src/navigation/RootNavigation.tsx +4 -7
  109. package/template/src/navigation/TabBar.tsx +7 -6
  110. package/template/src/navigation/types.ts +10 -31
  111. package/template/src/screens/Login/Login.tsx +47 -47
  112. package/template/src/screens/OTP/OTPScreen.tsx +6 -9
  113. package/template/src/screens/components/ComponentsScreen.tsx +301 -0
  114. package/template/src/screens/home/HomeScreen.tsx +143 -1
  115. package/template/src/screens/home/hooks/useHomeData.ts +19 -5
  116. package/template/src/screens/index.tsx +1 -0
  117. package/template/src/screens/profile/Profile.tsx +139 -2
  118. package/template/src/screens/splash/Splash.tsx +44 -11
  119. package/template/src/sheetManager/sheets.tsx +1 -1
  120. package/template/tsconfig.json +14 -2
  121. package/template/types/globals.d.ts +43 -0
  122. package/template/types/index.ts +2 -6
  123. package/template/types/modules.d.ts +9 -0
  124. package/template/types/react-native-config.d.ts +0 -2
  125. package/.vscode/settings.json +0 -8
  126. package/CHANGELOG.md +0 -119
  127. package/CODE_OF_CONDUCT.md +0 -83
  128. package/CONTRIBUTING.md +0 -60
  129. package/local.properties +0 -1
  130. package/template/src/common/components/ImageCropPickerButton.tsx +0 -107
  131. package/template/src/common/components/PhotoTakingButton.tsx +0 -94
  132. package/template/src/common/helpers/imageHelpers.ts +0 -5
  133. package/template/src/common/helpers/inAppReviewHelper.ts +0 -30
  134. package/template/src/common/helpers/orientationHelpers.ts +0 -25
  135. package/template/src/common/helpers/shareHelpers.ts +0 -47
  136. package/template/src/common/utils/FeesCaalculation.tsx +0 -37
  137. package/template/src/common/utils/printData.tsx +0 -161
  138. package/template/src/common/validations/examples/TextInputWithValidation.tsx +0 -229
@@ -12,37 +12,59 @@
12
12
  "ios": "react-native run-ios",
13
13
  "lint": "eslint .",
14
14
  "start": "react-native start",
15
- "test": "jest"
15
+ "test": "jest",
16
+ "typecheck": "tsc --noEmit",
17
+ "postinstall": "patch-package",
18
+ "prepare": "husky"
19
+ },
20
+ "lint-staged": {
21
+ "*.{ts,tsx,js,jsx}": [
22
+ "eslint --fix",
23
+ "prettier --write"
24
+ ],
25
+ "*.{json,md,yml,yaml}": [
26
+ "prettier --write"
27
+ ]
16
28
  },
17
29
  "dependencies": {
18
- "@good-react-native/gradient-border": "1.0.2",
30
+ "@d11/react-native-fast-image": "8.13.0",
31
+ "@gorhom/bottom-sheet": "5.2.10",
19
32
  "@react-native-async-storage/async-storage": "2.2.0",
33
+ "@react-native-community/netinfo": "12.0.1",
34
+ "@react-native-firebase/analytics": "24.0.0",
35
+ "@react-native-firebase/app": "24.0.0",
36
+ "@react-native-firebase/messaging": "24.0.0",
20
37
  "@react-native-masked-view/masked-view": "0.3.2",
21
- "@react-navigation/bottom-tabs": "7.15.5",
22
- "@react-navigation/native": "7.1.33",
23
- "@react-navigation/native-stack": "7.14.4",
38
+ "@react-navigation/bottom-tabs": "7.15.9",
39
+ "@react-navigation/native": "7.2.2",
40
+ "@react-navigation/native-stack": "7.14.11",
24
41
  "@reduxjs/toolkit": "2.11.2",
25
- "axios": "1.13.6",
42
+ "@shopify/flash-list": "2.3.1",
43
+ "axios": "^1.18.1",
26
44
  "babel-plugin-transform-remove-console": "6.9.4",
27
- "dayjs": "1.11.19",
45
+ "dayjs": "1.11.20",
28
46
  "intl": "1.2.5",
29
- "lodash": "4.17.23",
47
+ "lodash": "^4.18.1",
30
48
  "react": "19.2.3",
31
49
  "react-dom": "19.2.3",
32
- "react-native": "0.84.1",
50
+ "react-native": "0.85.2",
33
51
  "react-native-actions-sheet": "10.1.2",
34
52
  "react-native-config": "1.6.1",
35
53
  "react-native-device-info": "15.0.2",
36
- "react-native-gesture-handler": "2.30.0",
54
+ "react-native-gesture-handler": "2.31.1",
55
+ "react-native-haptic-feedback": "2.3.3",
37
56
  "react-native-keyboard-aware-scroll-view": "0.9.5",
38
57
  "react-native-linear-gradient": "2.8.3",
39
58
  "react-native-localization": "2.3.2",
40
- "react-native-reanimated": "4.2.2",
59
+ "react-native-permissions": "5.5.1",
60
+ "react-native-reanimated": "4.3.0",
41
61
  "react-native-restart": "0.0.27",
42
62
  "react-native-safe-area-context": "5.7.0",
43
63
  "react-native-screens": "4.24.0",
44
64
  "react-native-sfsymbols": "1.2.2",
65
+ "react-native-size-matters": "0.4.2",
45
66
  "react-native-snackbar": "3.0.1",
67
+ "react-native-svg": "15.15.4",
46
68
  "react-native-vector-icons": "10.3.0",
47
69
  "react-native-worklets": "0.7.4",
48
70
  "react-redux": "9.2.0",
@@ -51,26 +73,38 @@
51
73
  "redux-persist-transform-filter": "0.0.22"
52
74
  },
53
75
  "devDependencies": {
54
- "@babel/core": "7.29.0",
55
- "@babel/preset-env": "7.29.0",
76
+ "@babel/core": "^7.29.7",
77
+ "@babel/preset-env": "7.29.2",
56
78
  "@babel/runtime": "7.28.6",
57
79
  "@react-native-community/cli": "20.1.2",
58
80
  "@react-native-community/cli-platform-android": "20.1.2",
59
81
  "@react-native-community/cli-platform-ios": "20.1.2",
60
- "@react-native/babel-preset": "0.84.1",
61
- "@react-native/eslint-config": "0.84.1",
62
- "@react-native/metro-config": "0.84.1",
63
- "@react-native/typescript-config": "0.84.1",
82
+ "@react-native/babel-preset": "0.85.2",
83
+ "@react-native/eslint-config": "0.85.2",
84
+ "@react-native/jest-preset": "0.85.2",
85
+ "@react-native/metro-config": "0.85.2",
86
+ "@react-native/typescript-config": "0.85.2",
64
87
  "@types/jest": "30.0.0",
65
88
  "@types/react": "19.2.14",
66
89
  "@types/react-test-renderer": "19.1.0",
67
- "eslint": "10.0.2",
68
- "jest": "30.2.0",
90
+ "babel-plugin-module-resolver": "5.0.3",
91
+ "eslint": "^8.57.1",
92
+ "eslint-plugin-import": "^2.31.0",
93
+ "husky": "9.1.7",
94
+ "jest": "^30.4.2",
95
+ "lint-staged": "16.4.0",
96
+ "patch-package": "8.0.1",
69
97
  "prettier": "3.8.1",
70
98
  "react-test-renderer": "19.2.3",
71
99
  "typescript": "5.9.3"
72
100
  },
73
101
  "engines": {
74
102
  "node": ">=20"
103
+ },
104
+ "overrides": {
105
+ "jest-mock": "^30.0.0",
106
+ "jest-runtime": "^30.0.0",
107
+ "jest-snapshot": "^30.0.0",
108
+ "jest-environment-node": "^30.0.0"
75
109
  }
76
110
  }
@@ -0,0 +1,3 @@
1
+ module.exports = {
2
+ assets: ['./resources/fonts'],
3
+ };
File without changes
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * CI helper: write/update KEY=VALUE pairs in an env file.
5
+ * Used in Codemagic / Bitrise / GitHub Actions to sync version codes,
6
+ * build numbers, or any other env var into .env.<environment> before build.
7
+ *
8
+ * Usage:
9
+ * node scripts/ci-sync-env.cjs <envFile> KEY=value [KEY=value ...]
10
+ *
11
+ * Example (bump Android version code to next Play Store value):
12
+ * node scripts/ci-sync-env.cjs .env.production ANDROID_VERSION_CODE=42 IOS_BUILD_NUMBER=42
13
+ */
14
+
15
+ const fs = require('fs');
16
+ const path = require('path');
17
+
18
+ function parseArgs(argv) {
19
+ const [, , envFile, ...pairs] = argv;
20
+ if (!envFile) {
21
+ console.error(
22
+ 'Usage: node scripts/ci-sync-env.cjs <envFile> KEY=value [KEY=value ...]',
23
+ );
24
+ process.exit(1);
25
+ }
26
+ const updates = {};
27
+ for (const pair of pairs) {
28
+ const eq = pair.indexOf('=');
29
+ if (eq === -1) {
30
+ console.error(`Skipping invalid pair (no '='): ${pair}`);
31
+ continue;
32
+ }
33
+ const key = pair.slice(0, eq).trim();
34
+ const value = pair.slice(eq + 1);
35
+ updates[key] = value;
36
+ }
37
+ return { envFile: path.resolve(process.cwd(), envFile), updates };
38
+ }
39
+
40
+ function applyUpdates(envFile, updates) {
41
+ let content = '';
42
+ if (fs.existsSync(envFile)) {
43
+ content = fs.readFileSync(envFile, 'utf8');
44
+ }
45
+ const lines = content.split('\n');
46
+ const seen = new Set();
47
+ const output = lines.map((line) => {
48
+ const eq = line.indexOf('=');
49
+ if (eq === -1) return line;
50
+ const key = line.slice(0, eq).trim();
51
+ if (key in updates) {
52
+ seen.add(key);
53
+ return `${key}=${updates[key]}`;
54
+ }
55
+ return line;
56
+ });
57
+ for (const key of Object.keys(updates)) {
58
+ if (!seen.has(key)) {
59
+ if (output.length && output[output.length - 1] !== '') output.push('');
60
+ output.push(`${key}=${updates[key]}`);
61
+ }
62
+ }
63
+ fs.writeFileSync(envFile, output.join('\n'), 'utf8');
64
+ }
65
+
66
+ const { envFile, updates } = parseArgs(process.argv);
67
+ applyUpdates(envFile, updates);
68
+ console.log(`Updated ${envFile}:`);
69
+ for (const [k, v] of Object.entries(updates)) {
70
+ console.log(` ${k}=${v}`);
71
+ }
@@ -0,0 +1,8 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="120" height="120" viewBox="0 0 120 120" fill="none">
2
+ <g fill="#2F6BFF">
3
+ <rect x="25" y="24" width="13" height="72" rx="3"/>
4
+ <rect x="25" y="24" width="33" height="13" rx="3"/>
5
+ <rect x="25" y="52" width="26" height="12" rx="3"/>
6
+ <path d="M101 42 C101 32 91 28 83 28 C73 28 67 35 67 44 C67 52 75 56 84 59 C93 62 101 66 101 76 C101 86 91 91 83 91 C73 91 67 85 66 77" fill="none" stroke="#2F6BFF" stroke-width="13" stroke-linecap="round" stroke-linejoin="round"/>
7
+ </g>
8
+ </svg>
@@ -0,0 +1,9 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="120" height="120" viewBox="0 0 120 120" fill="none">
2
+ <rect width="120" height="120" rx="30" fill="#0D1124"/>
3
+ <g fill="#FFFFFF">
4
+ <rect x="25" y="24" width="13" height="72" rx="3"/>
5
+ <rect x="25" y="24" width="33" height="13" rx="3"/>
6
+ <rect x="25" y="52" width="26" height="12" rx="3"/>
7
+ <path d="M101 42 C101 32 91 28 83 28 C73 28 67 35 67 44 C67 52 75 56 84 59 C93 62 101 66 101 76 C101 86 91 91 83 91 C73 91 67 85 66 77" fill="none" stroke="#FFFFFF" stroke-width="13" stroke-linecap="round" stroke-linejoin="round"/>
8
+ </g>
9
+ </svg>
@@ -0,0 +1,15 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="120" height="120" viewBox="0 0 120 120" fill="none">
2
+ <defs>
3
+ <linearGradient id="chip" x1="0" y1="0" x2="1" y2="1">
4
+ <stop offset="0" stop-color="#0A1230"/>
5
+ <stop offset="1" stop-color="#1B45B8"/>
6
+ </linearGradient>
7
+ </defs>
8
+ <rect width="120" height="120" rx="30" fill="url(#chip)"/>
9
+ <g fill="#6BA0FF">
10
+ <rect x="25" y="24" width="13" height="72" rx="3"/>
11
+ <rect x="25" y="24" width="33" height="13" rx="3"/>
12
+ <rect x="25" y="52" width="26" height="12" rx="3"/>
13
+ <path d="M101 42 C101 32 91 28 83 28 C73 28 67 35 67 44 C67 52 75 56 84 59 C93 62 101 66 101 76 C101 86 91 91 83 91 C73 91 67 85 66 77" fill="none" stroke="#6BA0FF" stroke-width="13" stroke-linecap="round" stroke-linejoin="round"/>
14
+ </g>
15
+ </svg>
@@ -0,0 +1,18 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="460" height="140" viewBox="0 0 460 140" fill="none">
2
+ <defs>
3
+ <linearGradient id="chip" x1="0" y1="0" x2="1" y2="1">
4
+ <stop offset="0" stop-color="#0A1230"/>
5
+ <stop offset="1" stop-color="#1B45B8"/>
6
+ </linearGradient>
7
+ </defs>
8
+ <rect width="460" height="140" rx="22" fill="#06080F"/>
9
+ <rect x="28" y="22" width="96" height="96" rx="24" fill="url(#chip)"/>
10
+ <g fill="#6BA0FF" transform="translate(28,22) scale(0.8)">
11
+ <rect x="25" y="24" width="13" height="72" rx="3"/>
12
+ <rect x="25" y="24" width="33" height="13" rx="3"/>
13
+ <rect x="25" y="52" width="26" height="12" rx="3"/>
14
+ <path d="M101 42 C101 32 91 28 83 28 C73 28 67 35 67 44 C67 52 75 56 84 59 C93 62 101 66 101 76 C101 86 91 91 83 91 C73 91 67 85 66 77" fill="none" stroke="#6BA0FF" stroke-width="13" stroke-linecap="round" stroke-linejoin="round"/>
15
+ </g>
16
+ <text x="150" y="68" font-family="Verdana,Arial,sans-serif" font-size="30" font-weight="700" fill="#EEF2FF" letter-spacing="4">FADY SHAWKY</text>
17
+ <text x="152" y="94" font-family="Verdana,Arial,sans-serif" font-size="13" fill="#5E6A98" letter-spacing="3">react native magic</text>
18
+ </svg>
@@ -0,0 +1,87 @@
1
+ import React from 'react';
2
+ import {Modal, Pressable, StyleSheet, View, ViewStyle} from 'react-native';
3
+ import {useTheme} from '../../core/theme/ThemeProvider';
4
+ import {CommonSizes} from '../../core/theme/commonSizes';
5
+ import {RTLAwareText} from './RTLAwareText';
6
+
7
+ interface AppBottomSheetProps {
8
+ visible: boolean;
9
+ onClose: () => void;
10
+ title?: string;
11
+ children: React.ReactNode;
12
+ }
13
+
14
+ /**
15
+ * A bottom-anchored sheet built on react-native's Modal.
16
+ * Slides up from the bottom with a dimmed midnight backdrop; tapping the
17
+ * backdrop closes it while taps on the card are absorbed.
18
+ */
19
+ export function AppBottomSheet({
20
+ visible,
21
+ onClose,
22
+ title,
23
+ children,
24
+ }: AppBottomSheetProps): JSX.Element {
25
+ const {theme} = useTheme();
26
+
27
+ const cardStyle: ViewStyle = {
28
+ backgroundColor: theme.colors.grayScale_0,
29
+ borderTopLeftRadius: CommonSizes.borderRadius.xLarge,
30
+ borderTopRightRadius: CommonSizes.borderRadius.xLarge,
31
+ padding: CommonSizes.spacing.large,
32
+ paddingBottom: CommonSizes.spacing.xxxLarge,
33
+ };
34
+
35
+ const handleStyle: ViewStyle = {
36
+ width: 40,
37
+ height: 4,
38
+ borderRadius: CommonSizes.borderRadius.full,
39
+ backgroundColor: theme.colors.grayScale_50,
40
+ };
41
+
42
+ return (
43
+ <Modal
44
+ transparent
45
+ visible={visible}
46
+ animationType="slide"
47
+ onRequestClose={onClose}>
48
+ <View style={styles.container}>
49
+ <Pressable
50
+ style={styles.backdrop}
51
+ onPress={onClose}
52
+ accessibilityRole="button"
53
+ accessibilityLabel="Close"
54
+ />
55
+ <Pressable style={cardStyle} onPress={event => event.stopPropagation()}>
56
+ <View style={styles.handleWrapper}>
57
+ <View style={handleStyle} />
58
+ </View>
59
+ {title ? (
60
+ <RTLAwareText style={[theme.text.bodyXLargeBold, styles.title]}>
61
+ {title}
62
+ </RTLAwareText>
63
+ ) : null}
64
+ {children}
65
+ </Pressable>
66
+ </View>
67
+ </Modal>
68
+ );
69
+ }
70
+
71
+ const styles = StyleSheet.create({
72
+ container: {
73
+ flex: 1,
74
+ justifyContent: 'flex-end',
75
+ },
76
+ backdrop: {
77
+ ...StyleSheet.absoluteFill,
78
+ backgroundColor: 'rgba(6,8,15,0.6)',
79
+ },
80
+ handleWrapper: {
81
+ alignItems: 'center',
82
+ marginBottom: CommonSizes.spacing.large,
83
+ },
84
+ title: {
85
+ marginBottom: CommonSizes.spacing.medium,
86
+ },
87
+ });
@@ -0,0 +1,75 @@
1
+ import React, {useEffect, useRef} from 'react';
2
+ import {Animated, Pressable, StyleSheet, ViewStyle} from 'react-native';
3
+ import {useTheme} from '../../core/theme/ThemeProvider';
4
+ import {CommonSizes} from '../../core/theme/commonSizes';
5
+
6
+ interface AppSwitchProps {
7
+ value: boolean;
8
+ onValueChange: (v: boolean) => void;
9
+ disabled?: boolean;
10
+ }
11
+
12
+ const TRACK_WIDTH = 48;
13
+ const TRACK_HEIGHT = 28;
14
+ const THUMB_SIZE = 24;
15
+ const PADDING = (TRACK_HEIGHT - THUMB_SIZE) / 2;
16
+ const TRAVEL = TRACK_WIDTH - THUMB_SIZE - PADDING * 2;
17
+
18
+ export function AppSwitch(props: AppSwitchProps): JSX.Element {
19
+ const {value, onValueChange, disabled} = props;
20
+ const {theme} = useTheme();
21
+ const anim = useRef(new Animated.Value(value ? 1 : 0)).current;
22
+
23
+ useEffect(() => {
24
+ Animated.timing(anim, {
25
+ toValue: value ? 1 : 0,
26
+ duration: 180,
27
+ useNativeDriver: true,
28
+ }).start();
29
+ }, [value, anim]);
30
+
31
+ const translateX = anim.interpolate({
32
+ inputRange: [0, 1],
33
+ outputRange: [0, TRAVEL],
34
+ });
35
+
36
+ const trackStyle: ViewStyle = {
37
+ width: TRACK_WIDTH,
38
+ height: TRACK_HEIGHT,
39
+ borderRadius: CommonSizes.borderRadius.full,
40
+ padding: PADDING,
41
+ justifyContent: 'center',
42
+ backgroundColor: value
43
+ ? theme.colors.PlatinateBlue_400
44
+ : theme.colors.grayScale_50,
45
+ opacity: disabled ? 0.5 : 1,
46
+ };
47
+
48
+ return (
49
+ <Pressable
50
+ onPress={() => !disabled && onValueChange(!value)}
51
+ disabled={disabled}
52
+ hitSlop={8}
53
+ accessibilityRole="switch"
54
+ accessibilityState={{checked: value, disabled}}
55
+ style={trackStyle}>
56
+ <Animated.View
57
+ style={[styles.thumb, {transform: [{translateX}]}]}
58
+ />
59
+ </Pressable>
60
+ );
61
+ }
62
+
63
+ const styles = StyleSheet.create({
64
+ thumb: {
65
+ width: THUMB_SIZE,
66
+ height: THUMB_SIZE,
67
+ borderRadius: THUMB_SIZE / 2,
68
+ backgroundColor: '#FFFFFF',
69
+ shadowColor: '#06080F',
70
+ shadowOpacity: 0.2,
71
+ shadowRadius: 3,
72
+ shadowOffset: {width: 0, height: 1},
73
+ elevation: 3,
74
+ },
75
+ });
@@ -0,0 +1,161 @@
1
+ import React, {useMemo, useState} from 'react';
2
+ import {
3
+ KeyboardTypeOptions,
4
+ Pressable,
5
+ StyleSheet,
6
+ TextInput,
7
+ ViewStyle,
8
+ } from 'react-native';
9
+ import Svg, {Path} from 'react-native-svg';
10
+ import {useTheme} from '../../core/theme/ThemeProvider';
11
+ import {CommonSizes} from '../../core/theme/commonSizes';
12
+ import {RTLAwareText} from './RTLAwareText';
13
+ import {RTLAwareView} from './RTLAwareView';
14
+
15
+ interface AppTextInputProps {
16
+ label?: string;
17
+ value: string;
18
+ onChangeText: (t: string) => void;
19
+ placeholder?: string;
20
+ error?: string | null;
21
+ secureTextEntry?: boolean;
22
+ multiline?: boolean;
23
+ keyboardType?: KeyboardTypeOptions;
24
+ editable?: boolean;
25
+ }
26
+
27
+ function EyeIcon({open, color}: {open: boolean; color: string}): JSX.Element {
28
+ return (
29
+ <Svg width={22} height={22} viewBox="0 0 24 24" fill="none">
30
+ <Path
31
+ d="M2 12C3.7 7.6 7.5 5 12 5C16.5 5 20.3 7.6 22 12C20.3 16.4 16.5 19 12 19C7.5 19 3.7 16.4 2 12Z"
32
+ stroke={color}
33
+ strokeWidth={1.8}
34
+ strokeLinecap="round"
35
+ strokeLinejoin="round"
36
+ />
37
+ <Path
38
+ d="M12 15C13.66 15 15 13.66 15 12C15 10.34 13.66 9 12 9C10.34 9 9 10.34 9 12C9 13.66 10.34 15 12 15Z"
39
+ stroke={color}
40
+ strokeWidth={1.8}
41
+ strokeLinecap="round"
42
+ strokeLinejoin="round"
43
+ />
44
+ {!open ? (
45
+ <Path
46
+ d="M4 4L20 20"
47
+ stroke={color}
48
+ strokeWidth={1.8}
49
+ strokeLinecap="round"
50
+ />
51
+ ) : null}
52
+ </Svg>
53
+ );
54
+ }
55
+
56
+ export function AppTextInput(props: AppTextInputProps): JSX.Element {
57
+ const {
58
+ label,
59
+ value,
60
+ onChangeText,
61
+ placeholder,
62
+ error,
63
+ secureTextEntry,
64
+ multiline,
65
+ keyboardType,
66
+ editable = true,
67
+ } = props;
68
+ const {theme} = useTheme();
69
+ const [isFocused, setFocused] = useState(false);
70
+ const [hidden, setHidden] = useState(true);
71
+
72
+ const borderColor = useMemo(() => {
73
+ if (error) {
74
+ return theme.colors.error_400;
75
+ }
76
+ if (isFocused) {
77
+ return theme.colors.PlatinateBlue_400;
78
+ }
79
+ return theme.colors.grayScale_50;
80
+ }, [error, isFocused, theme.colors]);
81
+
82
+ const rowStyle: ViewStyle = {
83
+ flexDirection: 'row',
84
+ alignItems: multiline ? 'flex-start' : 'center',
85
+ backgroundColor: theme.colors.grayScale_0,
86
+ borderColor,
87
+ borderWidth: CommonSizes.borderWidth.medium,
88
+ borderRadius: CommonSizes.borderRadius.large,
89
+ paddingHorizontal: CommonSizes.spacing.xLarge,
90
+ paddingVertical: multiline
91
+ ? CommonSizes.spacing.large
92
+ : CommonSizes.spacing.medium,
93
+ minHeight: multiline ? 96 : undefined,
94
+ opacity: editable ? 1 : 0.6,
95
+ };
96
+
97
+ const inputDynamicStyle = {
98
+ color: theme.colors.grayScale_700,
99
+ textAlignVertical: multiline ? ('top' as const) : ('center' as const),
100
+ minHeight: multiline ? 72 : undefined,
101
+ };
102
+
103
+ return (
104
+ <RTLAwareView style={styles.container}>
105
+ {label ? (
106
+ <RTLAwareText
107
+ style={[theme.text.bodyMediumBold, {color: theme.colors.grayScale_700}]}>
108
+ {label}
109
+ </RTLAwareText>
110
+ ) : null}
111
+ <RTLAwareView style={rowStyle}>
112
+ <TextInput
113
+ style={[styles.input, theme.text.bodyLargeRegular, inputDynamicStyle]}
114
+ value={value}
115
+ onChangeText={onChangeText}
116
+ placeholder={placeholder}
117
+ placeholderTextColor={theme.colors.grayScale_200}
118
+ secureTextEntry={secureTextEntry ? hidden : false}
119
+ multiline={multiline}
120
+ keyboardType={keyboardType}
121
+ editable={editable}
122
+ onFocus={() => setFocused(true)}
123
+ onBlur={() => setFocused(false)}
124
+ selectionColor={theme.colors.PlatinateBlue_400}
125
+ />
126
+ {secureTextEntry ? (
127
+ <Pressable
128
+ onPress={() => setHidden(prev => !prev)}
129
+ hitSlop={8}
130
+ accessibilityRole="button"
131
+ accessibilityLabel={hidden ? 'Show password' : 'Hide password'}
132
+ style={styles.eyeButton}>
133
+ <EyeIcon open={!hidden} color={theme.colors.grayScale_200} />
134
+ </Pressable>
135
+ ) : null}
136
+ </RTLAwareView>
137
+ {error ? (
138
+ <RTLAwareText
139
+ style={[theme.text.bodySmallRegular, {color: theme.colors.error_400}]}>
140
+ {error}
141
+ </RTLAwareText>
142
+ ) : null}
143
+ </RTLAwareView>
144
+ );
145
+ }
146
+
147
+ const styles = StyleSheet.create({
148
+ container: {
149
+ width: '100%',
150
+ flexDirection: 'column',
151
+ gap: CommonSizes.spacing.medium,
152
+ } as ViewStyle,
153
+ input: {
154
+ flex: 1,
155
+ padding: 0,
156
+ },
157
+ eyeButton: {
158
+ paddingStart: CommonSizes.spacing.medium,
159
+ alignSelf: 'center',
160
+ },
161
+ });
@@ -0,0 +1,75 @@
1
+ import React from 'react';
2
+ import {Image, StyleSheet} from 'react-native';
3
+ import LinearGradient from 'react-native-linear-gradient';
4
+ import {BrandGradients, GradientDirection} from '../../core/theme/brand';
5
+ import {RTLAwareText} from './RTLAwareText';
6
+
7
+ interface AvatarProps {
8
+ name?: string;
9
+ uri?: string;
10
+ size?: number;
11
+ }
12
+
13
+ const DEFAULT_SIZE = 44;
14
+
15
+ function getInitials(name?: string): string {
16
+ if (!name) {
17
+ return '?';
18
+ }
19
+ const parts = name.trim().split(/\s+/).filter(Boolean);
20
+ if (parts.length === 0) {
21
+ return '?';
22
+ }
23
+ if (parts.length === 1) {
24
+ return parts[0].charAt(0).toUpperCase();
25
+ }
26
+ return (parts[0].charAt(0) + parts[parts.length - 1].charAt(0)).toUpperCase();
27
+ }
28
+
29
+ /**
30
+ * A circular avatar. With `uri` it renders the image; otherwise it shows the
31
+ * name's initials (1–2 letters, '?' when absent) in white on a gradient circle.
32
+ * Initials scale with `size`.
33
+ */
34
+ export function Avatar({name, uri, size = DEFAULT_SIZE}: AvatarProps): JSX.Element {
35
+ const circleStyle = {
36
+ width: size,
37
+ height: size,
38
+ borderRadius: size / 2,
39
+ };
40
+
41
+ if (uri) {
42
+ return <Image source={{uri}} style={[circleStyle, styles.image]} />;
43
+ }
44
+
45
+ return (
46
+ <LinearGradient
47
+ colors={BrandGradients.primary}
48
+ start={GradientDirection.start}
49
+ end={GradientDirection.end}
50
+ style={[circleStyle, styles.gradient]}>
51
+ <RTLAwareText
52
+ style={[
53
+ styles.initials,
54
+ {fontSize: size * 0.4, lineHeight: size * 0.5},
55
+ ]}>
56
+ {getInitials(name)}
57
+ </RTLAwareText>
58
+ </LinearGradient>
59
+ );
60
+ }
61
+
62
+ const styles = StyleSheet.create({
63
+ image: {
64
+ resizeMode: 'cover',
65
+ },
66
+ gradient: {
67
+ alignItems: 'center',
68
+ justifyContent: 'center',
69
+ },
70
+ initials: {
71
+ color: '#FFFFFF',
72
+ fontWeight: 'bold',
73
+ textAlign: 'center',
74
+ },
75
+ });