@fadyshawky/react-native-magic 2.0.8 → 2.1.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.
- package/README.md +55 -214
- package/package.json +9 -2
- package/scripts/askPackageName.js +165 -0
- package/template/.env.example +9 -0
- package/template/App.tsx +20 -16
- package/template/CHANGELOG.md +25 -0
- package/template/docs/ARCHITECTURE.md +27 -0
- package/template/docs/BEST_PRACTICES.md +33 -0
- package/template/docs/CUSTOMIZATION.md +53 -0
- package/template/index.js +1 -0
- package/template/package.json +36 -90
- package/template/src/common/components/AppStatusBar.tsx +24 -0
- package/template/src/common/components/SnackbarProvider.tsx +11 -0
- package/template/src/core/api/serverHeaders.ts +35 -64
- package/template/src/core/config/index.ts +13 -0
- package/template/src/core/store/store.tsx +3 -53
- package/template/src/core/theme/colors.ts +3 -0
- package/template/src/core/theme/commonSizes.ts +3 -0
- package/template/src/core/theme/fonts.ts +3 -0
- package/template/src/core/utils/stringUtils.ts +2 -45
- package/template/tsconfig.json +5 -1
- package/template.config.js +4 -2
package/README.md
CHANGED
|
@@ -1,266 +1,107 @@
|
|
|
1
1
|
# ReactNativeMagic
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**Plug and play** – create your app and start developing without hassle.
|
|
4
|
+
|
|
5
|
+
A production-ready React Native template with TypeScript, Redux, React Navigation, and a scalable architecture (Uprise-style). Use it to bootstrap new apps with one command.
|
|
4
6
|
|
|
5
7
|
## Requirements
|
|
6
8
|
|
|
7
|
-
- Node.js >=
|
|
9
|
+
- **Node.js >= 20** ([Download](https://nodejs.org/en/download/))
|
|
8
10
|
- JDK >= 11 ([Download](https://www.oracle.com/java/technologies/downloads/))
|
|
9
|
-
- Ruby >= 2.7.5 (for iOS)
|
|
10
|
-
- Xcode (for iOS)
|
|
11
|
-
- Android Studio (for Android) ([Download](https://developer.android.com/studio))
|
|
11
|
+
- Ruby >= 2.7.5 (for iOS)
|
|
12
|
+
- Xcode (for iOS) / Android Studio (for Android)
|
|
12
13
|
|
|
13
|
-
##
|
|
14
|
+
## Quick start
|
|
14
15
|
|
|
15
16
|
```bash
|
|
16
17
|
npx @react-native-community/cli init YourAppName --template @fadyshawky/react-native-magic
|
|
17
18
|
cd YourAppName
|
|
18
19
|
```
|
|
19
20
|
|
|
20
|
-
|
|
21
|
+
Optional: set your bundle ID at creation:
|
|
21
22
|
|
|
22
23
|
```bash
|
|
23
|
-
|
|
24
|
-
```
|
|
25
|
-
|
|
26
|
-
## Project Structure
|
|
27
|
-
|
|
24
|
+
npx @react-native-community/cli init YourAppName --template @fadyshawky/react-native-magic --package-name com.yourcompany.yourapp
|
|
28
25
|
```
|
|
29
|
-
YourAppName/
|
|
30
|
-
├── src/
|
|
31
|
-
│ ├── components/ # Reusable UI components
|
|
32
|
-
│ ├── screens/ # Screen components
|
|
33
|
-
│ ├── navigation/ # Navigation configurations
|
|
34
|
-
│ ├── services/ # API services and other external services
|
|
35
|
-
│ ├── store/ # State management
|
|
36
|
-
│ │ ├── slices/ # Redux slices
|
|
37
|
-
│ │ └── index.ts # Store configuration
|
|
38
|
-
│ ├── theme/ # Theme configurations
|
|
39
|
-
│ ├── utils/ # Utility functions
|
|
40
|
-
│ └── types/ # TypeScript type definitions
|
|
41
|
-
├── android/
|
|
42
|
-
├── ios/
|
|
43
|
-
└── ...
|
|
44
|
-
```
|
|
45
|
-
|
|
46
|
-
## Features
|
|
47
|
-
|
|
48
|
-
### 1. Type Safety
|
|
49
|
-
|
|
50
|
-
- Full TypeScript support
|
|
51
|
-
- Pre-configured tsconfig.json
|
|
52
|
-
- Type definitions for all components
|
|
53
|
-
|
|
54
|
-
### 2. Navigation
|
|
55
|
-
|
|
56
|
-
- React Navigation v6
|
|
57
|
-
- Type-safe navigation
|
|
58
|
-
- Bottom tabs setup
|
|
59
|
-
- Stack navigation setup
|
|
60
|
-
|
|
61
|
-
Documentation: [React Navigation](https://reactnavigation.org/docs/getting-started)
|
|
62
|
-
|
|
63
|
-
### 3. State Management
|
|
64
|
-
|
|
65
|
-
- Redux Toolkit
|
|
66
|
-
- Async storage integration
|
|
67
|
-
- Predefined store setup
|
|
68
26
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
### 4. Environment Variables
|
|
72
|
-
|
|
73
|
-
- React Native Config integration
|
|
74
|
-
- Secure environment configuration
|
|
75
|
-
- Type-safe environment variables
|
|
76
|
-
|
|
77
|
-
Documentation: [React Native Config](https://github.com/luggit/react-native-config)
|
|
78
|
-
|
|
79
|
-
### 5. Styling
|
|
80
|
-
|
|
81
|
-
- React Native Paper
|
|
82
|
-
- Custom theming system
|
|
83
|
-
- Dark mode support
|
|
84
|
-
|
|
85
|
-
Documentation: [React Native Paper](https://callstack.github.io/react-native-paper/)
|
|
86
|
-
|
|
87
|
-
### 6. Testing
|
|
88
|
-
|
|
89
|
-
- Jest configuration
|
|
90
|
-
- React Native Testing Library
|
|
91
|
-
- Example tests included
|
|
92
|
-
|
|
93
|
-
Documentation:
|
|
94
|
-
|
|
95
|
-
- [Jest](https://jestjs.io/docs/getting-started)
|
|
96
|
-
- [React Native Testing Library](https://callstack.github.io/react-native-testing-library/)
|
|
97
|
-
|
|
98
|
-
## Available Scripts
|
|
27
|
+
For iOS, install pods:
|
|
99
28
|
|
|
100
29
|
```bash
|
|
101
|
-
|
|
102
|
-
npm start
|
|
103
|
-
|
|
104
|
-
# Run on iOS
|
|
105
|
-
npm run ios
|
|
106
|
-
|
|
107
|
-
# Run on Android
|
|
108
|
-
npm run android
|
|
109
|
-
|
|
110
|
-
# Run tests
|
|
111
|
-
npm test
|
|
112
|
-
|
|
113
|
-
# Lint code
|
|
114
|
-
npm run lint
|
|
115
|
-
|
|
116
|
-
# Type checking
|
|
117
|
-
npm run typescript
|
|
118
|
-
```
|
|
119
|
-
|
|
120
|
-
## Dependencies
|
|
121
|
-
|
|
122
|
-
### Production Dependencies
|
|
123
|
-
|
|
124
|
-
```json
|
|
125
|
-
{
|
|
126
|
-
"@react-navigation/bottom-tabs": "^6.x",
|
|
127
|
-
"@react-navigation/native": "^6.x",
|
|
128
|
-
"@react-navigation/native-stack": "^6.x",
|
|
129
|
-
"@reduxjs/toolkit": "^1.x",
|
|
130
|
-
"react-native-paper": "^5.x",
|
|
131
|
-
"react-native-safe-area-context": "^4.x",
|
|
132
|
-
"react-native-screens": "^3.x",
|
|
133
|
-
"@react-native-async-storage/async-storage": "^1.x",
|
|
134
|
-
"react-native-config": "^1.x"
|
|
135
|
-
}
|
|
136
|
-
```
|
|
137
|
-
|
|
138
|
-
### Development Dependencies
|
|
139
|
-
|
|
140
|
-
```json
|
|
141
|
-
{
|
|
142
|
-
"@testing-library/react-native": "^11.x",
|
|
143
|
-
"@types/jest": "^29.x",
|
|
144
|
-
"@types/react": "^18.x",
|
|
145
|
-
"typescript": "^4.x",
|
|
146
|
-
"jest": "^29.x"
|
|
147
|
-
}
|
|
30
|
+
cd ios && pod install && cd ..
|
|
148
31
|
```
|
|
149
32
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
### iOS Build Issues
|
|
153
|
-
|
|
154
|
-
1. Pod installation fails:
|
|
33
|
+
Then run:
|
|
155
34
|
|
|
156
35
|
```bash
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
pod install
|
|
36
|
+
npm start
|
|
37
|
+
npm run ios # or: npm run android
|
|
160
38
|
```
|
|
161
39
|
|
|
162
|
-
|
|
40
|
+
## First steps after creating your app
|
|
163
41
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
npm run ios
|
|
169
|
-
```
|
|
42
|
+
1. **App name & bundle ID** – Set at init (or you’ll be prompted for package name if you didn’t pass `--package-name`). See [CUSTOMIZATION.md](template/docs/CUSTOMIZATION.md#app-name-and-bundle-id).
|
|
43
|
+
2. **API** – Copy `.env.example` to `.env` and set `API_BASE_URL` (and other vars) for your backend.
|
|
44
|
+
3. **Theme** – Edit `src/core/theme/colors.ts` (and `fonts.ts`, `commonSizes.ts` if needed) for your brand.
|
|
45
|
+
4. **Config** – Optional: adjust `src/core/config/index.ts` for feature toggles or app-level constants.
|
|
170
46
|
|
|
171
|
-
|
|
47
|
+
## Documentation
|
|
172
48
|
|
|
173
|
-
|
|
49
|
+
In your generated project you’ll have:
|
|
174
50
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
cd ..
|
|
179
|
-
npm run android
|
|
180
|
-
```
|
|
51
|
+
- **[docs/ARCHITECTURE.md](template/docs/ARCHITECTURE.md)** – Layers, folder map, data flow, SOLID.
|
|
52
|
+
- **[docs/CUSTOMIZATION.md](template/docs/CUSTOMIZATION.md)** – App name, bundle ID, API, theme, adding a screen/slice/language.
|
|
53
|
+
- **[docs/BEST_PRACTICES.md](template/docs/BEST_PRACTICES.md)** – Code style, structure, testing, security, upgrades.
|
|
181
54
|
|
|
182
|
-
|
|
183
|
-
Create a `local.properties` file in the android folder with your SDK path:
|
|
55
|
+
## Project structure (in your app)
|
|
184
56
|
|
|
185
|
-
```properties
|
|
186
|
-
sdk.dir=/Users/USERNAME/Library/Android/sdk
|
|
187
57
|
```
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
```typescript
|
|
196
|
-
export const theme = {
|
|
197
|
-
colors: {
|
|
198
|
-
primary: "#YOUR_COLOR",
|
|
199
|
-
// ...
|
|
200
|
-
},
|
|
201
|
-
// ...
|
|
202
|
-
};
|
|
58
|
+
src/
|
|
59
|
+
├── common/ # Shared components, localization, helpers, validations, utils
|
|
60
|
+
├── core/ # Store (Redux), API, theme, config
|
|
61
|
+
├── navigation/ # Auth stack, main stack, tabs
|
|
62
|
+
├── screens/ # Feature screens
|
|
63
|
+
└── sheetManager/ # Action sheets
|
|
203
64
|
```
|
|
204
65
|
|
|
205
|
-
|
|
66
|
+
## Scripts
|
|
206
67
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
68
|
+
| Command | Description |
|
|
69
|
+
|---------|-------------|
|
|
70
|
+
| `npm start` | Start Metro bundler |
|
|
71
|
+
| `npm run ios` | Run on iOS |
|
|
72
|
+
| `npm run android` | Run on Android |
|
|
73
|
+
| `npm test` | Run tests |
|
|
74
|
+
| `npm run lint` | Lint code |
|
|
210
75
|
|
|
211
|
-
|
|
76
|
+
## Versioning
|
|
212
77
|
|
|
213
|
-
|
|
78
|
+
- **React Native**: ^0.84.x (current stable at release).
|
|
79
|
+
- **React**: ^19.2.x.
|
|
80
|
+
- **Node**: >= 20 (LTS).
|
|
214
81
|
|
|
215
|
-
|
|
216
|
-
API_URL=https://api.example.com
|
|
217
|
-
ENV=development
|
|
218
|
-
```
|
|
82
|
+
Dependencies use **caret (^)** so your app can get patch/minor updates independently.
|
|
219
83
|
|
|
220
|
-
|
|
84
|
+
## Common issues
|
|
221
85
|
|
|
222
|
-
|
|
223
|
-
import Config from "react-native-config";
|
|
86
|
+
**iOS – Pod install fails**
|
|
224
87
|
|
|
225
|
-
|
|
88
|
+
```bash
|
|
89
|
+
cd ios && pod deintegrate && pod install && cd ..
|
|
226
90
|
```
|
|
227
91
|
|
|
228
|
-
|
|
92
|
+
**Android – Gradle / SDK**
|
|
229
93
|
|
|
230
|
-
|
|
94
|
+
- Run `./gradlew clean` in `android/`.
|
|
95
|
+
- Ensure `android/local.properties` has `sdk.dir` set to your Android SDK path.
|
|
231
96
|
|
|
232
|
-
|
|
233
|
-
2. Create your feature branch (`git checkout -b feature/AmazingFeature`)
|
|
234
|
-
3. Commit your changes (`git commit -m 'Add some AmazingFeature'`)
|
|
235
|
-
4. Push to the branch (`git push origin feature/AmazingFeature`)
|
|
236
|
-
5. Open a Pull Request
|
|
97
|
+
**Upgrading React Native**
|
|
237
98
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
- [React Native Documentation](https://reactnative.dev/docs/getting-started)
|
|
241
|
-
- [TypeScript Documentation](https://www.typescriptlang.org/docs/)
|
|
242
|
-
- [React Navigation Documentation](https://reactnavigation.org/docs/getting-started)
|
|
243
|
-
- [Redux Toolkit Documentation](https://redux-toolkit.js.org/introduction/getting-started)
|
|
244
|
-
- [React Native Paper Documentation](https://callstack.github.io/react-native-paper/)
|
|
99
|
+
Use [React Native Upgrade Helper](https://react-native-community.github.io/upgrade-helper/) (select current → target version) and apply the suggested changes.
|
|
245
100
|
|
|
246
101
|
## License
|
|
247
102
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
## Support
|
|
251
|
-
|
|
252
|
-
If you find this template helpful, consider buying me a beer! 🍺
|
|
253
|
-
|
|
254
|
-
<a href="https://www.buymeacoffee.com/fadytshawke"><img src="https://img.buymeacoffee.com/button-api/?text=Buy me a beer&emoji=🍺&slug=fadytshawke&button_colour=FFDD00&font_colour=000000&font_family=Cookie&outline_colour=000000&coffee_colour=ffffff" /></a>
|
|
103
|
+
MIT – see [LICENSE.md](LICENSE.md).
|
|
255
104
|
|
|
256
105
|
## Author
|
|
257
106
|
|
|
258
|
-
Fady Shawky
|
|
259
|
-
|
|
260
|
-
- GitHub: [@fadyshawky](https://github.com/fadyshawky)
|
|
261
|
-
|
|
262
|
-
## Acknowledgments
|
|
263
|
-
|
|
264
|
-
- React Native Team
|
|
265
|
-
- React Navigation Team
|
|
266
|
-
- All contributors who help maintain this template
|
|
107
|
+
Fady Shawky – [GitHub](https://github.com/fadyshawky)
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fadyshawky/react-native-magic",
|
|
3
|
-
"version": "2.0
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "2.1.0",
|
|
4
|
+
"description": "Plug-and-play React Native template: TypeScript, Redux, React Navigation, scalable architecture. Init and start developing without hassle.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react-native-magic",
|
|
7
7
|
"react-native",
|
|
@@ -110,6 +110,13 @@
|
|
|
110
110
|
"scripts": {
|
|
111
111
|
"test": "exit 0"
|
|
112
112
|
},
|
|
113
|
+
"peerDependencies": {
|
|
114
|
+
"react": "^19.2.0",
|
|
115
|
+
"react-native": "^0.84.0"
|
|
116
|
+
},
|
|
117
|
+
"engines": {
|
|
118
|
+
"node": ">=20"
|
|
119
|
+
},
|
|
113
120
|
"repository": {
|
|
114
121
|
"type": "git",
|
|
115
122
|
"url": "https://github.com/fadyshawky/ReactNativeMagic.git"
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Post-init script: if the user did not pass --package-name at init,
|
|
5
|
+
* prompt for a package name and apply it to .env and iOS project.
|
|
6
|
+
* Runs with cwd = the new project directory.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const readline = require('readline');
|
|
12
|
+
|
|
13
|
+
const PACKAGE_NAME_REGEX = /^([a-zA-Z][a-zA-Z0-9_]*\.)+[a-zA-Z][a-zA-Z0-9_]*$/;
|
|
14
|
+
|
|
15
|
+
function prompt(question) {
|
|
16
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
17
|
+
return new Promise((resolve) => {
|
|
18
|
+
rl.question(question, (answer) => {
|
|
19
|
+
rl.close();
|
|
20
|
+
resolve((answer || '').trim());
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function validatePackageName(packageName) {
|
|
26
|
+
const parts = packageName.split('.');
|
|
27
|
+
if (parts.length < 2) {
|
|
28
|
+
return 'Package name must have at least two segments (e.g. com.app)';
|
|
29
|
+
}
|
|
30
|
+
if (!PACKAGE_NAME_REGEX.test(packageName)) {
|
|
31
|
+
return 'Package name can only contain letters, numbers, underscores and dots';
|
|
32
|
+
}
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function ensureEnvHasPackageIds(envPath, packageName) {
|
|
37
|
+
const fullPath = path.join(process.cwd(), envPath);
|
|
38
|
+
let content = '';
|
|
39
|
+
if (fs.existsSync(fullPath)) {
|
|
40
|
+
content = fs.readFileSync(fullPath, 'utf8');
|
|
41
|
+
}
|
|
42
|
+
const lines = content.split('\n');
|
|
43
|
+
const updated = lines.map((line) => {
|
|
44
|
+
const eq = line.indexOf('=');
|
|
45
|
+
if (eq === -1) return line;
|
|
46
|
+
const key = line.slice(0, eq).trim();
|
|
47
|
+
if (key === 'APP_ID' || key === 'ANDROID_APP_ID') {
|
|
48
|
+
return `${key}=${packageName}`;
|
|
49
|
+
}
|
|
50
|
+
return line;
|
|
51
|
+
});
|
|
52
|
+
const hasAppId = lines.some((l) => /^APP_ID=/.test(l) || /^ANDROID_APP_ID=/.test(l));
|
|
53
|
+
if (!hasAppId) {
|
|
54
|
+
if (updated.length && updated[updated.length - 1] !== '') {
|
|
55
|
+
updated.push('');
|
|
56
|
+
}
|
|
57
|
+
updated.push(`APP_ID=${packageName}`);
|
|
58
|
+
updated.push(`ANDROID_APP_ID=${packageName}`);
|
|
59
|
+
}
|
|
60
|
+
fs.writeFileSync(fullPath, updated.join('\n'), 'utf8');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function getCurrentIosBundleId() {
|
|
64
|
+
const iosDir = path.join(process.cwd(), 'ios');
|
|
65
|
+
if (!fs.existsSync(iosDir)) return null;
|
|
66
|
+
const entries = fs.readdirSync(iosDir, { withFileTypes: true });
|
|
67
|
+
const pbxprojPaths = entries
|
|
68
|
+
.filter((d) => d.isDirectory() && d.name.endsWith('.xcodeproj'))
|
|
69
|
+
.map((d) => path.join(iosDir, d.name, 'project.pbxproj'))
|
|
70
|
+
.filter((p) => fs.existsSync(p));
|
|
71
|
+
const customPattern = /PRODUCT_BUNDLE_IDENTIFIER = "([^"]+)"/;
|
|
72
|
+
for (const pbx of pbxprojPaths) {
|
|
73
|
+
const content = fs.readFileSync(pbx, 'utf8');
|
|
74
|
+
const customMatch = content.match(customPattern);
|
|
75
|
+
if (customMatch) {
|
|
76
|
+
const id = customMatch[1];
|
|
77
|
+
if (!id.includes('$(')) return id;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function updateIosBundleId(packageName) {
|
|
84
|
+
const iosDir = path.join(process.cwd(), 'ios');
|
|
85
|
+
if (!fs.existsSync(iosDir)) return;
|
|
86
|
+
|
|
87
|
+
const entries = fs.readdirSync(iosDir, { withFileTypes: true });
|
|
88
|
+
const pbxprojPaths = entries
|
|
89
|
+
.filter((d) => d.isDirectory() && d.name.endsWith('.xcodeproj'))
|
|
90
|
+
.map((d) => path.join(iosDir, d.name, 'project.pbxproj'))
|
|
91
|
+
.filter((p) => fs.existsSync(p));
|
|
92
|
+
|
|
93
|
+
for (const pbx of pbxprojPaths) {
|
|
94
|
+
let content = fs.readFileSync(pbx, 'utf8');
|
|
95
|
+
const pattern = /PRODUCT_BUNDLE_IDENTIFIER = "org\.reactjs\.native\.example\.\$\(PRODUCT_NAME:rfc1034identifier\)"/g;
|
|
96
|
+
if (pattern.test(content)) {
|
|
97
|
+
content = content.replace(
|
|
98
|
+
/PRODUCT_BUNDLE_IDENTIFIER = "org\.reactjs\.native\.example\.\$\(PRODUCT_NAME:rfc1034identifier\)"/g,
|
|
99
|
+
`PRODUCT_BUNDLE_IDENTIFIER = "${packageName}"`
|
|
100
|
+
);
|
|
101
|
+
fs.writeFileSync(pbx, content, 'utf8');
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function main() {
|
|
107
|
+
const existingBundleId = getCurrentIosBundleId();
|
|
108
|
+
if (existingBundleId) {
|
|
109
|
+
// User passed --package-name; sync to .env for Android
|
|
110
|
+
const envPath = path.join(process.cwd(), '.env');
|
|
111
|
+
const examplePath = path.join(process.cwd(), '.env.example');
|
|
112
|
+
if (!fs.existsSync(envPath) && fs.existsSync(examplePath)) {
|
|
113
|
+
fs.copyFileSync(examplePath, envPath);
|
|
114
|
+
}
|
|
115
|
+
ensureEnvHasPackageIds('.env', existingBundleId);
|
|
116
|
+
for (const f of ['.env.development', '.env.staging', '.env.production']) {
|
|
117
|
+
if (fs.existsSync(path.join(process.cwd(), f))) {
|
|
118
|
+
ensureEnvHasPackageIds(f, existingBundleId);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
console.log(`\nSynced package name ${existingBundleId} to .env (Android).\n`);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
console.log('\nYou did not pass --package-name. Set a bundle ID (Android applicationId / iOS PRODUCT_BUNDLE_IDENTIFIER) now.\n');
|
|
126
|
+
|
|
127
|
+
const answer = await prompt('Package name (e.g. com.company.app), or press Enter to skip: ');
|
|
128
|
+
|
|
129
|
+
if (!answer) {
|
|
130
|
+
console.log('\nSkipped. You can set APP_ID and ANDROID_APP_ID in .env later, and set the iOS bundle ID in Xcode or use react-native-rename.\n');
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const err = validatePackageName(answer);
|
|
135
|
+
if (err) {
|
|
136
|
+
console.error('\nInvalid package name:', err);
|
|
137
|
+
process.exit(1);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const packageName = answer;
|
|
141
|
+
|
|
142
|
+
// Ensure .env exists
|
|
143
|
+
const envPath = path.join(process.cwd(), '.env');
|
|
144
|
+
const examplePath = path.join(process.cwd(), '.env.example');
|
|
145
|
+
if (!fs.existsSync(envPath) && fs.existsSync(examplePath)) {
|
|
146
|
+
fs.copyFileSync(examplePath, envPath);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
ensureEnvHasPackageIds('.env', packageName);
|
|
150
|
+
const envFiles = ['.env.development', '.env.staging', '.env.production'];
|
|
151
|
+
for (const f of envFiles) {
|
|
152
|
+
if (fs.existsSync(path.join(process.cwd(), f))) {
|
|
153
|
+
ensureEnvHasPackageIds(f, packageName);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
updateIosBundleId(packageName);
|
|
158
|
+
|
|
159
|
+
console.log(`\nSet package name to ${packageName} in .env and iOS project.\n`);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
main().catch((e) => {
|
|
163
|
+
console.error(e);
|
|
164
|
+
process.exit(1);
|
|
165
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Copy this file to .env and set your values.
|
|
2
|
+
# Do not commit .env (it is in .gitignore).
|
|
3
|
+
|
|
4
|
+
ENV=development
|
|
5
|
+
API_BASE_URL=https://api.example.com
|
|
6
|
+
|
|
7
|
+
# Bundle ID (Android applicationId / iOS). Set at init with --package-name or when prompted.
|
|
8
|
+
APP_ID=com.yourcompany.yourapp
|
|
9
|
+
ANDROID_APP_ID=com.yourcompany.yourapp
|
package/template/App.tsx
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Sample React Native App
|
|
3
|
-
* https://github.com/facebook/react-native
|
|
4
|
-
*
|
|
5
2
|
* @format
|
|
6
3
|
*/
|
|
7
4
|
import React from 'react';
|
|
8
|
-
import {LogBox,
|
|
5
|
+
import {LogBox, View} from 'react-native';
|
|
9
6
|
import {SheetProvider} from 'react-native-actions-sheet';
|
|
10
|
-
import {SafeAreaProvider} from 'react-native-safe-area-context';
|
|
7
|
+
import {SafeAreaProvider, SafeAreaView} from 'react-native-safe-area-context';
|
|
11
8
|
import {Provider} from 'react-redux';
|
|
12
9
|
import {PersistGate} from 'redux-persist/integration/react';
|
|
10
|
+
import {AppStatusBar} from './src/common/components/AppStatusBar';
|
|
11
|
+
import {SnackbarProvider} from './src/common/components/SnackbarProvider';
|
|
13
12
|
import {LocalizationProvider} from './src/common/localization/LocalizationProvider';
|
|
14
13
|
import {RTLInitializer} from './src/common/localization/RTLInitializer';
|
|
15
14
|
import {useAppSelector} from './src/core/store/reduxHelpers';
|
|
16
15
|
import {persistor, store} from './src/core/store/store';
|
|
17
16
|
import {ThemeProvider, useTheme} from './src/core/theme/ThemeProvider';
|
|
17
|
+
import {NaturalColors} from './src/core/theme/colors';
|
|
18
18
|
import AppNavigator from './src/navigation/MainNavigation';
|
|
19
19
|
|
|
20
20
|
LogBox.ignoreAllLogs();
|
|
@@ -27,14 +27,16 @@ const ThemedApp = () => {
|
|
|
27
27
|
<RTLInitializer>
|
|
28
28
|
<LocalizationProvider initialLanguage={language}>
|
|
29
29
|
<SafeAreaProvider>
|
|
30
|
-
<
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
<
|
|
37
|
-
|
|
30
|
+
<View style={{flex: 1, backgroundColor: theme?.colors?.background_2 ?? NaturalColors.background_2}}>
|
|
31
|
+
<SafeAreaView style={{position: 'absolute'}} />
|
|
32
|
+
<AppStatusBar
|
|
33
|
+
barStyle={theme?.mode === 'dark' ? 'light-content' : 'dark-content'}
|
|
34
|
+
backgroundColor={theme?.colors?.background_2 ?? NaturalColors.background_2}
|
|
35
|
+
/>
|
|
36
|
+
<SheetProvider>
|
|
37
|
+
<AppNavigator />
|
|
38
|
+
</SheetProvider>
|
|
39
|
+
</View>
|
|
38
40
|
</SafeAreaProvider>
|
|
39
41
|
</LocalizationProvider>
|
|
40
42
|
</RTLInitializer>
|
|
@@ -45,9 +47,11 @@ function App(): React.JSX.Element {
|
|
|
45
47
|
return (
|
|
46
48
|
<Provider store={store}>
|
|
47
49
|
<PersistGate loading={null} persistor={persistor}>
|
|
48
|
-
<
|
|
49
|
-
<
|
|
50
|
-
|
|
50
|
+
<SnackbarProvider>
|
|
51
|
+
<ThemeProvider initialTheme="dark">
|
|
52
|
+
<ThemedApp />
|
|
53
|
+
</ThemeProvider>
|
|
54
|
+
</SnackbarProvider>
|
|
51
55
|
</PersistGate>
|
|
52
56
|
</Provider>
|
|
53
57
|
);
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [Unreleased]
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- Architecture aligned with Uprise-style layout: `common/`, `core/`, `navigation/`, `screens/`, `sheetManager/`.
|
|
8
|
+
- Single app config module (`src/core/config`) for API base URL and feature toggles; base URL from `.env` via react-native-config.
|
|
9
|
+
- `AppStatusBar` component to avoid deprecated Android status bar APIs.
|
|
10
|
+
- `.env.example` for API_BASE_URL and ENV; `.env` in `.gitignore`.
|
|
11
|
+
- Documentation: `docs/ARCHITECTURE.md`, `docs/CUSTOMIZATION.md`, `docs/BEST_PRACTICES.md`.
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
|
|
15
|
+
- Dependencies updated to caret (^) versions; React Native ^0.84, React ^19.2; Node >= 20.
|
|
16
|
+
- Store persist simplified: whitelist only `user` (accessToken, user) and `app` (language, isRTL); removed CryptoJS/deviceId encryption.
|
|
17
|
+
- API layer uses `src/core/config` for base URL; request interceptor adds Bearer token and locale; response interceptor handles 401 (logout).
|
|
18
|
+
- Theme: added “Customize for your brand” comments in `colors.ts`, `fonts.ts`, `commonSizes.ts`.
|
|
19
|
+
- Bootstrap order: Provider → PersistGate → SnackbarProvider → ThemeProvider → ThemedApp (RTLInitializer → LocalizationProvider → SafeAreaProvider → AppStatusBar → SheetProvider → AppNavigator).
|
|
20
|
+
- `index.js` imports `./src/sheetManager/sheets` so sheets are registered.
|
|
21
|
+
|
|
22
|
+
### Template contract
|
|
23
|
+
|
|
24
|
+
- `template.config.js`: `placeholderName`, `titlePlaceholder`, `templateDir`.
|
|
25
|
+
- Bundle ID set at init via `--package-name`; app name and title replaced by CLI.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Architecture
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
This app uses a layered structure: **UI** (screens, components) → **navigation** → **core** (store, api, theme, config) and **common** (components, localization, helpers).
|
|
6
|
+
|
|
7
|
+
## Folder map
|
|
8
|
+
|
|
9
|
+
| Folder | Purpose |
|
|
10
|
+
|--------|--------|
|
|
11
|
+
| `src/common/` | Shared UI components, localization (i18n), helpers, validations, hooks, urls, utils |
|
|
12
|
+
| `src/core/` | Store (Redux + persist), API client, theme, config, services, hooks |
|
|
13
|
+
| `src/navigation/` | Navigator setup, auth stack, main stack, tab bar, header components |
|
|
14
|
+
| `src/screens/` | Feature screens (Login, Home, Profile, etc.); each may have local `components/` and `hooks/` |
|
|
15
|
+
| `src/sheetManager/` | Action sheet registration |
|
|
16
|
+
|
|
17
|
+
## Data flow
|
|
18
|
+
|
|
19
|
+
- **Auth**: Token is stored in Redux (`user.accessToken`). Navigation shows Auth stack when there is no token, Main stack when there is.
|
|
20
|
+
- **API**: Single axios instance in `src/core/api/serverHeaders.ts`. Base URL comes from `src/core/config` (and `.env`). Request interceptor adds `Authorization: Bearer` and `locale`; response interceptor handles 401 (e.g. logout).
|
|
21
|
+
- **Redux**: Slices live under `src/core/store/<domain>/` (e.g. `app`, `user`). Persist whitelist controls what is saved to AsyncStorage.
|
|
22
|
+
|
|
23
|
+
## SOLID mapping
|
|
24
|
+
|
|
25
|
+
- **SRP**: One domain per store folder; one primary concern per component file; theme split into colors, fonts, sizes, consts, styles.
|
|
26
|
+
- **OCP**: Extend by adding screens, slices, or routes without changing existing stack logic; extend theme by editing theme files.
|
|
27
|
+
- **DIP**: Core (API, store) does not depend on UI; screens depend on core via hooks/selectors; config abstracts environment.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Best practices
|
|
2
|
+
|
|
3
|
+
## Code style
|
|
4
|
+
|
|
5
|
+
- Use **TypeScript** strictly; avoid `any` where possible.
|
|
6
|
+
- Use typed Redux: `useAppSelector`, `useAppDispatch`, and `createAppAsyncThunk` with `RootState` / `AppDispatch`.
|
|
7
|
+
- Run `npm run lint` and fix issues.
|
|
8
|
+
|
|
9
|
+
## Structure
|
|
10
|
+
|
|
11
|
+
- **Single responsibility**: One concern per module/folder; keep screens thin (logic in hooks or actions).
|
|
12
|
+
- Put shared UI in `src/common/components/`.
|
|
13
|
+
- Use the API helpers from `src/core/api` (get, post, put, deleteApi); do not create extra axios instances.
|
|
14
|
+
|
|
15
|
+
## Testing
|
|
16
|
+
|
|
17
|
+
- Use **Jest** and **React Native Testing Library** for unit and component tests.
|
|
18
|
+
- Add tests for store logic and important components; run `npm test`.
|
|
19
|
+
|
|
20
|
+
## Performance
|
|
21
|
+
|
|
22
|
+
- Avoid creating new objects/functions in render when they are passed as props to children.
|
|
23
|
+
- Use stable keys for list items.
|
|
24
|
+
|
|
25
|
+
## Security
|
|
26
|
+
|
|
27
|
+
- Do not commit secrets; use `.env` for API keys and base URL (and add `.env` to `.gitignore`).
|
|
28
|
+
- Persist only necessary fields (use the persist whitelist in the store).
|
|
29
|
+
|
|
30
|
+
## LTS / upgrades
|
|
31
|
+
|
|
32
|
+
- The template targets **Node >= 20** and current stable **React Native** (e.g. 0.84.x).
|
|
33
|
+
- To upgrade React Native, use [React Native Upgrade Helper](https://react-native-community.github.io/upgrade-helper/): select your current version and the target version, then apply the suggested changes to `package.json`, `android/`, `ios/`, and config files.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Customization
|
|
2
|
+
|
|
3
|
+
## App name and bundle ID
|
|
4
|
+
|
|
5
|
+
Set at project creation:
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx @react-native-community/cli init YourAppName --template @fadyshawky/react-native-magic --package-name com.yourcompany.yourapp
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
- **App name**: Replaces the template placeholder in `app.json` and file names.
|
|
12
|
+
- **Bundle ID** (`--package-name`): Sets Android `applicationId` and iOS `PRODUCT_BUNDLE_IDENTIFIER` and restructures Android package folders.
|
|
13
|
+
|
|
14
|
+
If you omit `--package-name`, the template will **prompt you** after init to enter a package name (e.g. `com.company.app`). You can enter it then, or press Enter to skip and set `APP_ID` / `ANDROID_APP_ID` in `.env` later and the iOS bundle ID in Xcode or via [react-native-rename](https://www.npmjs.com/package/react-native-rename).
|
|
15
|
+
|
|
16
|
+
## API base URL and environment
|
|
17
|
+
|
|
18
|
+
1. Copy `.env.example` to `.env`.
|
|
19
|
+
2. Set `API_BASE_URL` (and any other keys) for your backend.
|
|
20
|
+
3. The app reads these via `react-native-config`; `src/core/config/index.ts` re-exports them for type-safe use.
|
|
21
|
+
|
|
22
|
+
Optional: use multiple env files (e.g. `.env.development`, `.env.staging`, `.env.production`) and build variants so each build uses the right URL.
|
|
23
|
+
|
|
24
|
+
## Theme
|
|
25
|
+
|
|
26
|
+
Edit these files for your brand (no need to touch components):
|
|
27
|
+
|
|
28
|
+
- **Colors**: `src/core/theme/colors.ts`
|
|
29
|
+
- **Fonts**: `src/core/theme/fonts.ts`
|
|
30
|
+
- **Sizes / spacing**: `src/core/theme/commonSizes.ts`
|
|
31
|
+
|
|
32
|
+
## Adding a new language
|
|
33
|
+
|
|
34
|
+
1. Add a new translation file under `src/common/localization/translations/`, e.g. `frLocalization.ts`, following the same shape as `commonLocalization.ts` or `loginLocalization.ts`.
|
|
35
|
+
2. In `src/common/localization/localization.ts`, import the new file and add it to the `localization` object, e.g. `fr: new LocalizedStrings(frLocalization)`.
|
|
36
|
+
3. Add the language to the `Languages` enum in `localization.ts` if you use it for switching (e.g. `fr = 'fr'`).
|
|
37
|
+
4. Use the new keys in your components via the existing `t(key, section)` or the relevant `localization.*` object.
|
|
38
|
+
|
|
39
|
+
## Adding a screen
|
|
40
|
+
|
|
41
|
+
1. Create a folder under `src/screens/<Feature>/` with `Feature.tsx` and optional `components/` and `hooks/`.
|
|
42
|
+
2. Register the screen in the right stack in `src/navigation/` (e.g. `AuthStack.tsx` or `MainStack.tsx`).
|
|
43
|
+
3. Add the route and component to the stack’s screen list.
|
|
44
|
+
|
|
45
|
+
## Adding a Redux slice
|
|
46
|
+
|
|
47
|
+
1. Create a folder under `src/core/store/<domain>/` with `*State.ts`, `*Slice.ts`, and optionally `*Actions.ts`.
|
|
48
|
+
2. Add the slice to `src/core/store/rootReducer.ts`.
|
|
49
|
+
3. If the slice should persist, add a `createWhitelistFilter('<domain>', ['field1', 'field2'])` entry in `src/core/store/store.tsx` persist config.
|
|
50
|
+
|
|
51
|
+
## Feature flags / app config
|
|
52
|
+
|
|
53
|
+
Toggle features or app-level constants in `src/core/config/index.ts` or via env vars read there (e.g. `enableRTL`).
|
package/template/index.js
CHANGED
package/template/package.json
CHANGED
|
@@ -1,116 +1,62 @@
|
|
|
1
1
|
{
|
|
2
|
-
"name": "
|
|
2
|
+
"name": "reactnativemagic",
|
|
3
3
|
"version": "0.0.1",
|
|
4
4
|
"private": true,
|
|
5
5
|
"scripts": {
|
|
6
6
|
"android": "react-native run-android",
|
|
7
|
-
"android:staging": "react-native run-android --variant=stagingDebug",
|
|
8
|
-
"android:staging-release": "react-native run-android --variant=stagingRelease",
|
|
9
|
-
"android:dev": "ENVFILE=.env.development react-native run-android --variant=developmentDebug",
|
|
10
|
-
"android:dev-release": "ENVFILE=.env.development react-native run-android --variant=developmentRelease",
|
|
11
|
-
"android:prod": "ENVFILE=.env.production react-native run-android --variant=productionDebug",
|
|
12
|
-
"android:prod-release": "ENVFILE=.env.production react-native run-android --variant=productionRelease",
|
|
13
7
|
"ios": "react-native run-ios",
|
|
14
8
|
"lint": "eslint .",
|
|
15
9
|
"start": "react-native start",
|
|
16
|
-
"start:development": "ENVFILE=.env.development react-native start --reset-cache",
|
|
17
|
-
"start:production": "ENVFILE=.env.production react-native start --reset-cache",
|
|
18
|
-
"start:staging": "ENVFILE=.env.staging react-native start --reset-cache",
|
|
19
10
|
"test": "jest"
|
|
20
11
|
},
|
|
21
12
|
"dependencies": {
|
|
22
|
-
"@
|
|
23
|
-
"@react-
|
|
24
|
-
"@react-native
|
|
25
|
-
"@react-native-
|
|
26
|
-
"@
|
|
27
|
-
"
|
|
28
|
-
"@react-native-community/slider": "^4.5.5",
|
|
29
|
-
"@react-navigation/bottom-tabs": "^7.2.0",
|
|
30
|
-
"@react-navigation/drawer": "^7.1.1",
|
|
31
|
-
"@react-navigation/elements": "^2.2.5",
|
|
32
|
-
"@react-navigation/material-top-tabs": "^7.1.0",
|
|
33
|
-
"@react-navigation/native": "^7.0.14",
|
|
34
|
-
"@react-navigation/native-stack": "^7.2.0",
|
|
35
|
-
"@reduxjs/toolkit": "^2.5.0",
|
|
36
|
-
"@shopify/flash-list": "^1.7.2",
|
|
37
|
-
"@types/intl": "^1.2.2",
|
|
38
|
-
"@types/jest": "^29.5.14",
|
|
39
|
-
"@types/lodash": "^4.17.13",
|
|
40
|
-
"@types/react-native-vector-icons": "^6.4.18",
|
|
41
|
-
"@types/react-redux": "^7.1.34",
|
|
42
|
-
"axios": "^1.7.9",
|
|
13
|
+
"@react-native-async-storage/async-storage": "^2.2.0",
|
|
14
|
+
"@react-navigation/bottom-tabs": "^7.14.0",
|
|
15
|
+
"@react-navigation/native": "^7.1.28",
|
|
16
|
+
"@react-navigation/native-stack": "^7.13.0",
|
|
17
|
+
"@reduxjs/toolkit": "^2.11.0",
|
|
18
|
+
"axios": "^1.13.0",
|
|
43
19
|
"babel-plugin-transform-remove-console": "^6.9.4",
|
|
44
|
-
"dayjs": "^1.11.
|
|
45
|
-
"detox": "^20.28.0",
|
|
20
|
+
"dayjs": "^1.11.19",
|
|
46
21
|
"intl": "^1.2.5",
|
|
47
|
-
"lodash": "^4.17.
|
|
48
|
-
"
|
|
49
|
-
"
|
|
50
|
-
"react": "^
|
|
51
|
-
"react-native": "^
|
|
52
|
-
"react-native-
|
|
53
|
-
"react-native-
|
|
54
|
-
"react-native-
|
|
55
|
-
"react-native-config": "^1.5.3",
|
|
56
|
-
"react-native-device-info": "^14.0.2",
|
|
57
|
-
"react-native-dropdown-select-list": "^2.0.5",
|
|
58
|
-
"react-native-gesture-handler": "^2.21.2",
|
|
59
|
-
"react-native-image-crop-picker": "^0.41.6",
|
|
60
|
-
"react-native-image-resource-generator": "^1.0.2",
|
|
61
|
-
"react-native-in-app-review": "^4.3.3",
|
|
62
|
-
"react-native-keyboard-aware-scroll-view": "^0.9.5",
|
|
22
|
+
"lodash": "^4.17.23",
|
|
23
|
+
"react": "^19.2.0",
|
|
24
|
+
"react-dom": "^19.2.0",
|
|
25
|
+
"react-native": "^0.84.0",
|
|
26
|
+
"react-native-actions-sheet": "^10.1.0",
|
|
27
|
+
"react-native-config": "^1.6.0",
|
|
28
|
+
"react-native-device-info": "^14.0.0",
|
|
29
|
+
"react-native-gesture-handler": "^2.30.0",
|
|
63
30
|
"react-native-localization": "^2.3.2",
|
|
64
|
-
"react-native-
|
|
65
|
-
"react-native-
|
|
66
|
-
"react-native-pager-view": "^6.6.1",
|
|
67
|
-
"react-native-permissions": "^5.2.1",
|
|
68
|
-
"react-native-reanimated": "^3.16.6",
|
|
69
|
-
"react-native-reanimated-carousel": "^3.5.1",
|
|
70
|
-
"react-native-restart": "^0.0.27",
|
|
71
|
-
"react-native-safe-area-context": "^5.0.0",
|
|
72
|
-
"react-native-screens": "^4.4.0",
|
|
73
|
-
"react-native-sfsymbols": "^1.2.2",
|
|
74
|
-
"react-native-share": "^12.0.3",
|
|
31
|
+
"react-native-safe-area-context": "^5.6.0",
|
|
32
|
+
"react-native-screens": "^4.24.0",
|
|
75
33
|
"react-native-snackbar": "^2.8.0",
|
|
76
|
-
"react-native-svg": "^15.10.1",
|
|
77
|
-
"react-native-tab-view": "^4.0.5",
|
|
78
|
-
"react-native-vector-icons": "^10.2.0",
|
|
79
|
-
"react-native-vision-camera": "^4.6.3",
|
|
80
|
-
"react-native-webview": "^13.12.5",
|
|
81
34
|
"react-redux": "^9.2.0",
|
|
82
35
|
"redux": "^5.0.1",
|
|
83
36
|
"redux-persist": "^6.0.0",
|
|
84
37
|
"redux-persist-transform-filter": "^0.0.22"
|
|
85
38
|
},
|
|
86
39
|
"devDependencies": {
|
|
87
|
-
"@babel/core": "^7.
|
|
40
|
+
"@babel/core": "^7.29.0",
|
|
88
41
|
"@babel/preset-env": "^7.26.0",
|
|
89
|
-
"@babel/runtime": "^7.
|
|
90
|
-
"@
|
|
91
|
-
"@react-native-community/cli": "
|
|
92
|
-
"@react-native-community/cli-platform-
|
|
93
|
-
"@react-native
|
|
94
|
-
"@react-native/
|
|
95
|
-
"@react-native/
|
|
96
|
-
"@react-native/
|
|
97
|
-
"@
|
|
98
|
-
"@types/react": "^
|
|
99
|
-
"@types/react-test-renderer": "^
|
|
100
|
-
"
|
|
101
|
-
"
|
|
102
|
-
"
|
|
103
|
-
"
|
|
104
|
-
"
|
|
105
|
-
"eslint-plugin-react-hooks": "^5.1.0",
|
|
106
|
-
"eslint-plugin-react-native": "^4.1.0",
|
|
107
|
-
"eslint-plugin-unused-imports": "^4.1.4",
|
|
108
|
-
"jest": "^29.7.0",
|
|
109
|
-
"prettier": "^2.8.8",
|
|
110
|
-
"react-test-renderer": "^18.3.1",
|
|
111
|
-
"typescript": "^5.0.4"
|
|
42
|
+
"@babel/runtime": "^7.28.0",
|
|
43
|
+
"@react-native-community/cli": "^20.1.0",
|
|
44
|
+
"@react-native-community/cli-platform-android": "^20.1.0",
|
|
45
|
+
"@react-native-community/cli-platform-ios": "^20.1.0",
|
|
46
|
+
"@react-native/babel-preset": "^0.84.0",
|
|
47
|
+
"@react-native/eslint-config": "^0.84.0",
|
|
48
|
+
"@react-native/metro-config": "^0.84.0",
|
|
49
|
+
"@react-native/typescript-config": "^0.84.0",
|
|
50
|
+
"@types/jest": "^30.0.0",
|
|
51
|
+
"@types/react": "^19.2.0",
|
|
52
|
+
"@types/react-test-renderer": "^19.1.0",
|
|
53
|
+
"eslint": "^10.0.0",
|
|
54
|
+
"jest": "^30.0.0",
|
|
55
|
+
"prettier": "^3.8.0",
|
|
56
|
+
"react-test-renderer": "^19.2.0",
|
|
57
|
+
"typescript": "^5.9.0"
|
|
112
58
|
},
|
|
113
59
|
"engines": {
|
|
114
|
-
"node": ">=
|
|
60
|
+
"node": ">=20"
|
|
115
61
|
}
|
|
116
62
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* StatusBar wrapper that avoids deprecated Android APIs when using edge-to-edge.
|
|
3
|
+
* On Android 15+ with edge-to-edge, Window.setStatusBarColor/setNavigationBarColor
|
|
4
|
+
* are deprecated. Passing backgroundColor on Android triggers those APIs, so we
|
|
5
|
+
* only pass it on iOS.
|
|
6
|
+
*/
|
|
7
|
+
import React from 'react';
|
|
8
|
+
import {Platform, StatusBar, StatusBarProps} from 'react-native';
|
|
9
|
+
|
|
10
|
+
type AppStatusBarProps = Omit<StatusBarProps, 'backgroundColor'> & {
|
|
11
|
+
backgroundColor?: string;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export function AppStatusBar({
|
|
15
|
+
backgroundColor,
|
|
16
|
+
...rest
|
|
17
|
+
}: AppStatusBarProps): React.JSX.Element {
|
|
18
|
+
return (
|
|
19
|
+
<StatusBar
|
|
20
|
+
{...rest}
|
|
21
|
+
backgroundColor={Platform.OS === 'ios' ? backgroundColor : undefined}
|
|
22
|
+
/>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Wrapper for snackbar/toast. The template uses react-native-snackbar directly.
|
|
5
|
+
* Replace with a custom implementation (e.g. context + showSnackbar) if needed.
|
|
6
|
+
*/
|
|
7
|
+
export const SnackbarProvider = ({
|
|
8
|
+
children = null,
|
|
9
|
+
}: {
|
|
10
|
+
children?: React.ReactNode;
|
|
11
|
+
}): React.JSX.Element => <>{children}</>;
|
|
@@ -1,86 +1,57 @@
|
|
|
1
|
-
import axios, {AxiosDefaults} from 'axios';
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
1
|
+
import axios, {AxiosDefaults, AxiosError, AxiosResponse} from 'axios';
|
|
2
|
+
import {store} from '../store/store';
|
|
3
|
+
import {setLogout} from '../store/user/userSlice';
|
|
4
|
+
import {API_BASE_URL} from '../config';
|
|
4
5
|
|
|
5
|
-
export const defaultHeaders:
|
|
6
|
-
'app-version': DeviceInfo.getVersion(),
|
|
7
|
-
'serial-number': DeviceInfo.getSerialNumberSync(),
|
|
6
|
+
export const defaultHeaders: HeadersInit = {
|
|
8
7
|
Connection: 'keep-alive',
|
|
9
8
|
'Content-Type': 'application/json',
|
|
10
9
|
};
|
|
10
|
+
|
|
11
11
|
declare type MethodData = {
|
|
12
12
|
url: AxiosDefaults['httpsAgent'];
|
|
13
13
|
data?: AxiosDefaults['data'];
|
|
14
14
|
config?: any;
|
|
15
15
|
};
|
|
16
16
|
|
|
17
|
-
const baseURL = 'https://sofapi.neo-dg.app/api/v1/mobile/';
|
|
18
|
-
|
|
19
17
|
const instance = axios.create({
|
|
20
|
-
baseURL:
|
|
18
|
+
baseURL: API_BASE_URL,
|
|
21
19
|
headers: {
|
|
22
20
|
...defaultHeaders,
|
|
23
21
|
},
|
|
24
22
|
});
|
|
25
23
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
},
|
|
40
|
-
// Don't double stringify the data
|
|
41
|
-
body: config.data ? config.data : undefined,
|
|
42
|
-
sslPinning: {
|
|
43
|
-
certs: ['STAR.neopayplus.com'],
|
|
44
|
-
allowInvalidCertificates: false,
|
|
45
|
-
validatesDomainName: true,
|
|
46
|
-
},
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
if (fetchOptions.method === 'GET' || fetchOptions.method === 'HEAD') {
|
|
50
|
-
delete fetchOptions.body;
|
|
51
|
-
}
|
|
24
|
+
instance.interceptors.request.use(
|
|
25
|
+
config => {
|
|
26
|
+
const state = store.getState();
|
|
27
|
+
const accessToken = state.user.accessToken;
|
|
28
|
+
const locale = state.app?.language ?? 'en';
|
|
29
|
+
if (accessToken) {
|
|
30
|
+
config.headers.Authorization = `Bearer ${accessToken}`;
|
|
31
|
+
}
|
|
32
|
+
config.headers.locale = locale;
|
|
33
|
+
return config;
|
|
34
|
+
},
|
|
35
|
+
error => Promise.reject(error),
|
|
36
|
+
);
|
|
52
37
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
.catch(error => {
|
|
66
|
-
console.error('error: ', error);
|
|
67
|
-
if (error === 'timeout') {
|
|
68
|
-
throw new Error(error);
|
|
69
|
-
} else if (JSON.parse(error?.bodyString)?.error) {
|
|
70
|
-
throw new Error(JSON.parse(error?.bodyString)?.error);
|
|
71
|
-
} else if (JSON.parse(error?.bodyString)?.message) {
|
|
72
|
-
throw new Error(JSON.parse(error?.bodyString)?.message);
|
|
73
|
-
}
|
|
74
|
-
});
|
|
75
|
-
};
|
|
76
|
-
} catch (error) {
|
|
77
|
-
console.info('error: ', error);
|
|
78
|
-
throw error;
|
|
38
|
+
instance.interceptors.response.use(
|
|
39
|
+
(response: AxiosResponse) => response,
|
|
40
|
+
async (error: AxiosError) => {
|
|
41
|
+
const originalRequest = error.config;
|
|
42
|
+
if (
|
|
43
|
+
originalRequest?.url?.includes('/login') ||
|
|
44
|
+
originalRequest?.url?.includes('/refresh-token')
|
|
45
|
+
) {
|
|
46
|
+
return Promise.reject(error);
|
|
47
|
+
}
|
|
48
|
+
if (error.response?.status === 401 || error.response?.status === 402) {
|
|
49
|
+
store.dispatch(setLogout());
|
|
79
50
|
}
|
|
80
|
-
|
|
81
|
-
}
|
|
51
|
+
return Promise.reject(error);
|
|
52
|
+
},
|
|
53
|
+
);
|
|
82
54
|
|
|
83
|
-
// Export HTTP methods using the configured instance
|
|
84
55
|
export const post = ({url, data, config}: MethodData) =>
|
|
85
56
|
instance.post(url, data, config);
|
|
86
57
|
export const get = ({url, config}: MethodData) => instance.get(url, config);
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* App config – single place for API base URL, env, and feature toggles.
|
|
3
|
+
* Change your app's API and feature toggles here and in .env.
|
|
4
|
+
*/
|
|
5
|
+
import Config from 'react-native-config';
|
|
6
|
+
|
|
7
|
+
const config = Config as Record<string, string | undefined>;
|
|
8
|
+
export const API_BASE_URL =
|
|
9
|
+
config.API_BASE_URL || config.API_URL || 'https://api.example.com';
|
|
10
|
+
export const ENV = config.ENV || config.ENVIRONMENT || 'development';
|
|
11
|
+
|
|
12
|
+
/** Optional: enable RTL layout (e.g. for Arabic). */
|
|
13
|
+
export const enableRTL = true;
|
|
@@ -1,49 +1,11 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import {Action, configureStore, ThunkAction} from '@reduxjs/toolkit';
|
|
3
|
-
import {
|
|
4
|
-
createTransform,
|
|
5
|
-
PersistConfig,
|
|
6
|
-
persistReducer,
|
|
7
|
-
persistStore,
|
|
8
|
-
} from 'redux-persist';
|
|
3
|
+
import {PersistConfig, persistReducer, persistStore} from 'redux-persist';
|
|
9
4
|
import {rootReducer, RootState} from './rootReducer';
|
|
10
5
|
import {Provider} from 'react-redux';
|
|
11
6
|
import {PersistGate} from 'redux-persist/integration/react';
|
|
12
7
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
13
8
|
import {createWhitelistFilter} from 'redux-persist-transform-filter';
|
|
14
|
-
import {getUniqueId} from 'react-native-device-info';
|
|
15
|
-
import CryptoJS from 'crypto-js';
|
|
16
|
-
|
|
17
|
-
let deviceId: string;
|
|
18
|
-
getUniqueId()
|
|
19
|
-
.then(res => {
|
|
20
|
-
deviceId = res;
|
|
21
|
-
})
|
|
22
|
-
.catch(error => {});
|
|
23
|
-
|
|
24
|
-
const encrypt = createTransform(
|
|
25
|
-
(inboundState, key) => {
|
|
26
|
-
if (!inboundState) {
|
|
27
|
-
return inboundState;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const cryptedText = CryptoJS.AES.encrypt(
|
|
31
|
-
JSON.stringify(inboundState),
|
|
32
|
-
deviceId,
|
|
33
|
-
);
|
|
34
|
-
|
|
35
|
-
return cryptedText?.toString();
|
|
36
|
-
},
|
|
37
|
-
(outboundState, key) => {
|
|
38
|
-
if (!outboundState) {
|
|
39
|
-
return outboundState;
|
|
40
|
-
}
|
|
41
|
-
const bytes = CryptoJS.AES.decrypt(outboundState, deviceId);
|
|
42
|
-
const decrypted = bytes.toString(CryptoJS.enc.Utf8);
|
|
43
|
-
|
|
44
|
-
return JSON.parse(decrypted);
|
|
45
|
-
},
|
|
46
|
-
);
|
|
47
9
|
|
|
48
10
|
const persistConfig: PersistConfig<RootState> = {
|
|
49
11
|
key: 'root',
|
|
@@ -51,11 +13,8 @@ const persistConfig: PersistConfig<RootState> = {
|
|
|
51
13
|
version: 1,
|
|
52
14
|
timeout: 1000,
|
|
53
15
|
transforms: [
|
|
54
|
-
createWhitelistFilter('
|
|
55
|
-
createWhitelistFilter('
|
|
56
|
-
createWhitelistFilter('services'),
|
|
57
|
-
createWhitelistFilter('providers'),
|
|
58
|
-
createWhitelistFilter('categories'),
|
|
16
|
+
createWhitelistFilter('user', ['accessToken', 'user']),
|
|
17
|
+
createWhitelistFilter('app', ['language', 'isRTL']),
|
|
59
18
|
],
|
|
60
19
|
};
|
|
61
20
|
|
|
@@ -66,16 +25,7 @@ export const store = configureStore({
|
|
|
66
25
|
middleware: getDefaultMiddleware =>
|
|
67
26
|
getDefaultMiddleware({
|
|
68
27
|
serializableCheck: false,
|
|
69
|
-
immutableCheck: {
|
|
70
|
-
warnAfter: 300,
|
|
71
|
-
ignoredPaths: [
|
|
72
|
-
'services.inquiredBill',
|
|
73
|
-
'providers.providers',
|
|
74
|
-
'categories.categories',
|
|
75
|
-
],
|
|
76
|
-
},
|
|
77
28
|
}),
|
|
78
|
-
devTools: process.env.NODE_ENV !== 'production',
|
|
79
29
|
});
|
|
80
30
|
|
|
81
31
|
export const persistor = persistStore(store);
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Safely converts any value to a string
|
|
3
|
-
*
|
|
2
|
+
* Safely converts any value to a string.
|
|
3
|
+
* Useful for error messages that might be objects.
|
|
4
4
|
*/
|
|
5
|
-
import CryptoJS from 'crypto-js';
|
|
6
|
-
|
|
7
5
|
export const ensureString = (value: any): string => {
|
|
8
6
|
if (value === null || value === undefined) {
|
|
9
7
|
return '';
|
|
@@ -31,47 +29,6 @@ export const ensureString = (value: any): string => {
|
|
|
31
29
|
return String(value);
|
|
32
30
|
};
|
|
33
31
|
|
|
34
|
-
export function decryptTripleDES(
|
|
35
|
-
encryptedBase64: string,
|
|
36
|
-
secretKeyBase64: string,
|
|
37
|
-
) {
|
|
38
|
-
try {
|
|
39
|
-
// Decode the key from base64
|
|
40
|
-
const decodedKey = CryptoJS.enc.Base64.parse(secretKeyBase64);
|
|
41
|
-
|
|
42
|
-
// Decode the encrypted value from base64
|
|
43
|
-
const encryptedWordArray = CryptoJS.enc.Base64.parse(encryptedBase64);
|
|
44
|
-
|
|
45
|
-
// Decrypt using Triple DES
|
|
46
|
-
const decrypted = CryptoJS.TripleDES.decrypt(
|
|
47
|
-
{ciphertext: encryptedWordArray},
|
|
48
|
-
decodedKey,
|
|
49
|
-
{
|
|
50
|
-
mode: CryptoJS.mode.ECB,
|
|
51
|
-
padding: CryptoJS.pad.Pkcs7,
|
|
52
|
-
},
|
|
53
|
-
);
|
|
54
|
-
|
|
55
|
-
let decryptedText = decrypted.toString(CryptoJS.enc.Utf8);
|
|
56
|
-
|
|
57
|
-
if (!decryptedText) {
|
|
58
|
-
throw new Error('Decryption resulted in empty string');
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
decryptedText = decryptedText.replace(/(.{4})/g, '$1-');
|
|
62
|
-
if (decryptedText.endsWith('-')) {
|
|
63
|
-
decryptedText = decryptedText.slice(0, -1);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
return decryptedText;
|
|
67
|
-
} catch (error) {
|
|
68
|
-
throw new Error(`Decryption failed`);
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// Example usage:
|
|
73
|
-
// const decrypted = decryptBase64('encrypted_base64_string', 'your_secret_key');
|
|
74
|
-
|
|
75
32
|
/**
|
|
76
33
|
* Converts a camelCase string to PascalCase with specific formatting
|
|
77
34
|
* Example: billerId -> BillerId
|
package/template/tsconfig.json
CHANGED
package/template.config.js
CHANGED