@fadyshawky/react-native-magic 2.0.9 → 2.1.1
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/.vscode/settings.json +2 -1
- package/README.md +55 -214
- package/package.json +9 -90
- package/scripts/askPackageName.js +165 -0
- package/template/.env.example +12 -0
- package/template/App.tsx +20 -16
- package/template/CHANGELOG.md +25 -0
- package/template/android/app/build.gradle +3 -3
- package/template/android/app/src/main/java/com/reactnativemagic/MainActivity.kt +8 -2
- package/template/android/app/src/main/java/com/reactnativemagic/MainApplication.kt +12 -29
- package/template/android/build.gradle +5 -5
- package/template/android/gradle/wrapper/gradle-wrapper.properties +1 -1
- 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 +44 -89
- package/template/src/common/components/AppStatusBar.tsx +24 -0
- package/template/src/common/components/Cards.tsx +1 -1
- package/template/src/common/components/EmptyView.tsx +1 -1
- package/template/src/common/components/OTPInput.tsx +1 -1
- package/template/src/common/components/PrimaryButton.tsx +5 -5
- package/template/src/common/components/PrimaryTextInput.tsx +4 -4
- package/template/src/common/components/RadioButton.tsx +1 -1
- package/template/src/common/components/RadioIcon.tsx +4 -4
- package/template/src/common/components/SnackbarProvider.tsx +11 -0
- package/template/src/common/components/TryAgain.tsx +3 -3
- package/template/src/common/validations/errorValidations.ts +1 -1
- package/template/src/core/api/serverHeaders.ts +38 -9
- 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/src/navigation/TabBar.tsx +5 -5
- package/template/src/screens/index.tsx +0 -15
- package/template/tsconfig.json +6 -1
- package/template.config.js +4 -2
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React, {FC, memo, useMemo} from 'react';
|
|
2
2
|
import {StyleSheet, View, ViewStyle} from 'react-native';
|
|
3
|
-
import {
|
|
3
|
+
import {PrimaryColors} from '../../core/theme/colors';
|
|
4
4
|
|
|
5
5
|
interface IProps {
|
|
6
6
|
isSelected: boolean;
|
|
@@ -41,14 +41,14 @@ const commonInnerCircle: ViewStyle = {
|
|
|
41
41
|
const styles = StyleSheet.create({
|
|
42
42
|
outerCircle: {
|
|
43
43
|
...commonOuterCircle,
|
|
44
|
-
borderColor:
|
|
44
|
+
borderColor: PrimaryColors.PlatinateBlue_400,
|
|
45
45
|
} as ViewStyle,
|
|
46
46
|
outerCircleSelected: {
|
|
47
47
|
...commonOuterCircle,
|
|
48
|
-
borderColor:
|
|
48
|
+
borderColor: PrimaryColors.PlatinateBlue_400,
|
|
49
49
|
} as ViewStyle,
|
|
50
50
|
innerCircle: {
|
|
51
51
|
...commonInnerCircle,
|
|
52
|
-
backgroundColor:
|
|
52
|
+
backgroundColor: PrimaryColors.PlatinateBlue_400,
|
|
53
53
|
} as ViewStyle,
|
|
54
54
|
});
|
|
@@ -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}</>;
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
TouchableOpacity,
|
|
7
7
|
View,
|
|
8
8
|
} from 'react-native';
|
|
9
|
-
import {
|
|
9
|
+
import {PrimaryColors} from '../../core/theme/colors';
|
|
10
10
|
import {CommonSizes} from '../../core/theme/commonSizes';
|
|
11
11
|
import {CommonStyles} from '../../core/theme/commonStyles';
|
|
12
12
|
import {localization} from '../localization/localization';
|
|
@@ -38,11 +38,11 @@ const styles = StyleSheet.create({
|
|
|
38
38
|
title: {
|
|
39
39
|
...CommonStyles.normalText,
|
|
40
40
|
textAlign: 'center',
|
|
41
|
-
marginBottom: CommonSizes.spacing.
|
|
41
|
+
marginBottom: CommonSizes.spacing.xSmall,
|
|
42
42
|
} as TextStyle,
|
|
43
43
|
description: {
|
|
44
44
|
...CommonStyles.normalText,
|
|
45
|
-
color:
|
|
45
|
+
color: PrimaryColors.PlatinateBlue_400,
|
|
46
46
|
textAlign: 'center',
|
|
47
47
|
textDecorationLine: 'underline',
|
|
48
48
|
} as TextStyle,
|
|
@@ -3,7 +3,7 @@ import {unwrapResult} from '@reduxjs/toolkit';
|
|
|
3
3
|
import {Alert} from 'react-native';
|
|
4
4
|
import {IErrorResult, ErrorRepresentationType} from '../../../types';
|
|
5
5
|
import Snackbar from 'react-native-snackbar';
|
|
6
|
-
|
|
6
|
+
|
|
7
7
|
export function handlePromiseResult(
|
|
8
8
|
promiseAction: Promise<any>,
|
|
9
9
|
successMessage?: string,
|
|
@@ -1,28 +1,57 @@
|
|
|
1
|
-
import axios, {AxiosDefaults} from 'axios';
|
|
2
|
-
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';
|
|
3
5
|
|
|
4
|
-
export const defaultHeaders:
|
|
5
|
-
'app-version': DeviceInfo.getVersion(),
|
|
6
|
-
'serial-number': DeviceInfo.getSerialNumberSync(),
|
|
6
|
+
export const defaultHeaders: HeadersInit = {
|
|
7
7
|
Connection: 'keep-alive',
|
|
8
8
|
'Content-Type': 'application/json',
|
|
9
9
|
};
|
|
10
|
+
|
|
10
11
|
declare type MethodData = {
|
|
11
12
|
url: AxiosDefaults['httpsAgent'];
|
|
12
13
|
data?: AxiosDefaults['data'];
|
|
13
14
|
config?: any;
|
|
14
15
|
};
|
|
15
16
|
|
|
16
|
-
const baseURL = '';
|
|
17
|
-
|
|
18
17
|
const instance = axios.create({
|
|
19
|
-
baseURL:
|
|
18
|
+
baseURL: API_BASE_URL,
|
|
20
19
|
headers: {
|
|
21
20
|
...defaultHeaders,
|
|
22
21
|
},
|
|
23
22
|
});
|
|
24
23
|
|
|
25
|
-
|
|
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
|
+
);
|
|
37
|
+
|
|
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());
|
|
50
|
+
}
|
|
51
|
+
return Promise.reject(error);
|
|
52
|
+
},
|
|
53
|
+
);
|
|
54
|
+
|
|
26
55
|
export const post = ({url, data, config}: MethodData) =>
|
|
27
56
|
instance.post(url, data, config);
|
|
28
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
|
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
useRTL,
|
|
17
17
|
useTranslation,
|
|
18
18
|
} from '../common/localization/LocalizationProvider';
|
|
19
|
-
import {
|
|
19
|
+
import {PrimaryColors} from '../core/theme/colors';
|
|
20
20
|
import {CommonSizes} from '../core/theme/commonSizes';
|
|
21
21
|
import {CommonStyles} from '../core/theme/commonStyles';
|
|
22
22
|
import {scaleHeight, scaleWidth} from '../core/theme/scaling';
|
|
@@ -125,11 +125,11 @@ const styles = StyleSheet.create({
|
|
|
125
125
|
flexDirection: 'row',
|
|
126
126
|
alignItems: 'center',
|
|
127
127
|
height: scaleHeight(130),
|
|
128
|
-
borderTopLeftRadius: CommonSizes.borderRadius.
|
|
129
|
-
borderTopRightRadius: CommonSizes.borderRadius.
|
|
128
|
+
borderTopLeftRadius: CommonSizes.borderRadius.xxLarge,
|
|
129
|
+
borderTopRightRadius: CommonSizes.borderRadius.xxLarge,
|
|
130
130
|
justifyContent: 'space-evenly',
|
|
131
131
|
...CommonStyles.dropShadow,
|
|
132
|
-
backgroundColor:
|
|
132
|
+
backgroundColor: PrimaryColors.PlatinateBlue_600,
|
|
133
133
|
position: 'absolute',
|
|
134
134
|
bottom: 0,
|
|
135
135
|
left: 0,
|
|
@@ -141,6 +141,6 @@ const styles = StyleSheet.create({
|
|
|
141
141
|
},
|
|
142
142
|
label: {},
|
|
143
143
|
labelFocused: {
|
|
144
|
-
color:
|
|
144
|
+
color: PrimaryColors.PlatinateBlue_400,
|
|
145
145
|
},
|
|
146
146
|
});
|
|
@@ -1,24 +1,9 @@
|
|
|
1
1
|
// Authentication Screens
|
|
2
2
|
export {Login} from './Login/Login';
|
|
3
3
|
export {OTPScreen as OTP} from './OTP/OTPScreen';
|
|
4
|
-
export {ForceChangePasswordScreen as ForceChangePassword} from './ForceChangePassword/ForceChangePasswordScreen';
|
|
5
4
|
|
|
6
5
|
// Main Navigation Screens
|
|
7
6
|
export {HomeScreen as Home} from './home/HomeScreen';
|
|
8
7
|
export {Profile} from './profile/Profile';
|
|
9
8
|
export {Splash} from './splash/Splash';
|
|
10
9
|
|
|
11
|
-
// Service Related Screens
|
|
12
|
-
export {Categories} from './Categories/Categories';
|
|
13
|
-
export {Services} from './Services/Services';
|
|
14
|
-
export {SingleService} from './SingleService/SingleService';
|
|
15
|
-
export {Providers} from './Providers/Providers';
|
|
16
|
-
export {InquiredBill} from './InquiredBill/InquiredBill';
|
|
17
|
-
export {PaymentConfirmation} from './PaymentConfirmation/PaymentConfirmation';
|
|
18
|
-
|
|
19
|
-
// History and Receipt Screens
|
|
20
|
-
export {History} from './History/History';
|
|
21
|
-
export {Favorites} from './Favorites/Favorites';
|
|
22
|
-
|
|
23
|
-
// Device and Card Related Screens
|
|
24
|
-
export {ReceiptScreen} from './ReceiptScreen/ReceiptScreen';
|
package/template/tsconfig.json
CHANGED
package/template.config.js
CHANGED