@fadyshawky/react-native-magic 2.2.1 → 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.
- package/LICENSE +21 -0
- package/README.md +90 -56
- package/index.js +4 -0
- package/package.json +8 -2
- package/scripts/askPackageName.js +10 -5
- package/template/.env.development +8 -6
- package/template/.env.example +15 -5
- package/template/.env.production +8 -6
- package/template/.env.staging +8 -6
- package/template/.eslintrc.js +14 -0
- package/template/.husky/pre-commit +1 -0
- package/template/App.tsx +47 -16
- package/template/__tests__/App.test.tsx +28 -10
- package/template/babel.config.js +20 -1
- package/template/docs/ARCHITECTURE.md +40 -10
- package/template/docs/BEST_PRACTICES.md +10 -1
- package/template/docs/CUSTOMIZATION.md +118 -5
- package/template/docs/design-system.html +1164 -0
- package/template/docs/wireframes.html +411 -0
- package/template/index.js +10 -0
- package/template/jest.config.js +16 -1
- package/template/jest.setup.js +61 -0
- package/template/package-lock.json +12178 -8293
- package/template/package.json +53 -20
- package/template/react-native.config.js +3 -0
- package/template/resources/fonts/.gitkeep +0 -0
- package/template/scripts/ci-sync-env.cjs +71 -0
- package/template/src/assets/brand/logo-mark.svg +8 -0
- package/template/src/assets/brand/logo-mono.svg +9 -0
- package/template/src/assets/brand/logo-primary.svg +15 -0
- package/template/src/assets/brand/wordmark-dark.svg +18 -0
- package/template/src/common/components/AppBottomSheet.tsx +87 -0
- package/template/src/common/components/AppSwitch.tsx +75 -0
- package/template/src/common/components/AppTextInput.tsx +161 -0
- package/template/src/common/components/Avatar.tsx +75 -0
- package/template/src/common/components/Badge.tsx +66 -0
- package/template/src/common/components/CardScroller.tsx +58 -0
- package/template/src/common/components/Cards.tsx +13 -7
- package/template/src/common/components/Carousel.tsx +196 -0
- package/template/src/common/components/Checkbox.tsx +85 -0
- package/template/src/common/components/Chip.tsx +55 -0
- package/template/src/common/components/Dropdown.tsx +202 -0
- package/template/src/common/components/ErrorBoundary.tsx +82 -0
- package/template/src/common/components/FlatListWrapper.tsx +4 -5
- package/template/src/common/components/ListItem.tsx +90 -0
- package/template/src/common/components/LoadingComponent.tsx +8 -2
- package/template/src/common/components/Logo.tsx +77 -0
- package/template/src/common/components/ModalDialog.tsx +141 -0
- package/template/src/common/components/NetworkBanner.tsx +47 -0
- package/template/src/common/components/OTPInput.tsx +0 -1
- package/template/src/common/components/PrimaryButton.tsx +0 -14
- package/template/src/common/components/PrimaryTextInput.tsx +66 -130
- package/template/src/common/components/RadioGroup.tsx +95 -0
- package/template/src/common/components/SafeText.tsx +4 -3
- package/template/src/common/components/SearchBar.tsx +7 -5
- package/template/src/common/components/SegmentedControl.tsx +77 -0
- package/template/src/common/components/Skeleton.tsx +47 -0
- package/template/src/common/components/TryAgain.tsx +4 -2
- package/template/src/common/helpers/arrayHelpers.ts +2 -2
- package/template/src/common/helpers/defaultKeyIdExtractor.ts +1 -1
- package/template/src/common/helpers/regexHelpers.ts +1 -2
- package/template/src/common/helpers/stringsHelpers.ts +0 -1
- package/template/src/common/hooks/useBackHandler.ts +5 -2
- package/template/src/common/hooks/useEventRegister.ts +1 -1
- package/template/src/common/hooks/useFlatListActions.ts +1 -1
- package/template/src/common/hooks/useWhyDidYouUpdate.ts +1 -1
- package/template/src/common/localization/LocalizationProvider.tsx +1 -1
- package/template/src/common/localization/RTLInitializer.tsx +1 -1
- package/template/src/common/localization/dateFormatter.ts +0 -1
- package/template/src/common/localization/intlFormatter.ts +0 -1
- package/template/src/common/localization/localization.ts +2 -2
- package/template/src/common/localization/translations/homeLocalization.ts +14 -0
- package/template/src/common/localization/translations/loginLocalization.ts +8 -0
- package/template/src/common/localization/translations/mainNavigationLocalization.ts +2 -0
- package/template/src/common/localization/translations/profileLocalization.ts +16 -0
- package/template/src/common/utils/index.tsx +0 -6
- package/template/src/common/validations/commonValidations.ts +2 -2
- package/template/src/core/api/errorHandler.ts +1 -1
- package/template/src/core/api/responseHandlers.ts +1 -3
- package/template/src/core/api/serverHeaders.ts +61 -12
- package/template/src/core/notifications/notificationAuth.ts +6 -0
- package/template/src/core/notifications/notificationService.ts +125 -0
- package/template/src/core/notifications/routeFromNotificationData.ts +32 -0
- package/template/src/core/store/categories/categoriesActions.ts +25 -0
- package/template/src/core/store/categories/categoriesSlice.ts +51 -0
- package/template/src/core/store/categories/categoriesState.ts +19 -0
- package/template/src/core/store/rootReducer.ts +2 -0
- package/template/src/core/store/store.tsx +6 -1
- package/template/src/core/store/user/userActions.ts +75 -14
- package/template/src/core/store/user/userSlice.ts +49 -26
- package/template/src/core/store/user/userState.ts +6 -4
- package/template/src/core/theme/ThemeProvider.tsx +5 -3
- package/template/src/core/theme/brand.ts +50 -0
- package/template/src/core/theme/colors.ts +113 -99
- package/template/src/core/theme/commonConsts.ts +2 -2
- package/template/src/core/theme/commonStyles.ts +1 -1
- package/template/src/core/theme/themes.ts +2 -0
- package/template/src/core/theme/types.ts +4 -2
- package/template/src/core/utils/stringUtils.ts +1 -1
- package/template/src/design-system/index.ts +2 -0
- package/template/src/design-system/tokens/brand.ts +6 -0
- package/template/src/design-system/tokens/index.ts +3 -0
- package/template/src/design-system/tokens/palette.ts +4 -0
- package/template/src/design-system/tokens/typography-spacing.ts +2 -0
- package/template/src/navigation/AuthStack.tsx +1 -4
- package/template/src/navigation/HeaderComponents.tsx +6 -3
- package/template/src/navigation/MainStack.tsx +18 -6
- package/template/src/navigation/RootNavigation.tsx +4 -7
- package/template/src/navigation/TabBar.tsx +7 -6
- package/template/src/navigation/types.ts +10 -31
- package/template/src/screens/Login/Login.tsx +47 -47
- package/template/src/screens/OTP/OTPScreen.tsx +6 -9
- package/template/src/screens/components/ComponentsScreen.tsx +301 -0
- package/template/src/screens/home/HomeScreen.tsx +143 -1
- package/template/src/screens/home/hooks/useHomeData.ts +19 -5
- package/template/src/screens/index.tsx +1 -0
- package/template/src/screens/profile/Profile.tsx +139 -2
- package/template/src/screens/splash/Splash.tsx +44 -11
- package/template/src/sheetManager/sheets.tsx +1 -1
- package/template/tsconfig.json +14 -2
- package/template/types/globals.d.ts +43 -0
- package/template/types/index.ts +2 -6
- package/template/types/modules.d.ts +9 -0
- package/template/types/react-native-config.d.ts +0 -2
- package/.vscode/settings.json +0 -8
- package/CHANGELOG.md +0 -119
- package/CODE_OF_CONDUCT.md +0 -83
- package/CONTRIBUTING.md +0 -60
- package/local.properties +0 -1
- package/template/src/common/components/ImageCropPickerButton.tsx +0 -107
- package/template/src/common/components/PhotoTakingButton.tsx +0 -94
- package/template/src/common/helpers/imageHelpers.ts +0 -5
- package/template/src/common/helpers/inAppReviewHelper.ts +0 -30
- package/template/src/common/helpers/orientationHelpers.ts +0 -25
- package/template/src/common/helpers/shareHelpers.ts +0 -47
- package/template/src/common/utils/FeesCaalculation.tsx +0 -37
- package/template/src/common/utils/printData.tsx +0 -161
- package/template/src/common/validations/examples/TextInputWithValidation.tsx +0 -229
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import InAppReview from 'react-native-in-app-review';
|
|
2
|
-
|
|
3
|
-
export function showInAppReview(
|
|
4
|
-
successAction?: () => void,
|
|
5
|
-
failAction?: (error: Error) => void,
|
|
6
|
-
onReviewNotAvailable?: () => void,
|
|
7
|
-
) {
|
|
8
|
-
if (InAppReview.isAvailable()) {
|
|
9
|
-
InAppReview.RequestInAppReview()
|
|
10
|
-
.then(hasFlowFinishedSuccessfully => {
|
|
11
|
-
// for Android:
|
|
12
|
-
// The flow has finished. The API does not indicate whether the user
|
|
13
|
-
// reviewed or not, or even whether the review dialog was shown. Thus, no
|
|
14
|
-
// matter the result, we continue our app flow.
|
|
15
|
-
|
|
16
|
-
// for iOS
|
|
17
|
-
// the flow launched successfully, The API does not indicate whether the user
|
|
18
|
-
// reviewed or not, or he/she closed flow yet as Android, Thus, no
|
|
19
|
-
// matter the result, we continue our app flow.
|
|
20
|
-
if (hasFlowFinishedSuccessfully) {
|
|
21
|
-
successAction?.();
|
|
22
|
-
}
|
|
23
|
-
})
|
|
24
|
-
.catch(error => {
|
|
25
|
-
failAction?.(error);
|
|
26
|
-
});
|
|
27
|
-
} else {
|
|
28
|
-
onReviewNotAvailable?.();
|
|
29
|
-
}
|
|
30
|
-
}
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import Orientation, {OrientationType} from 'react-native-orientation-locker';
|
|
2
|
-
import {isTablet} from '../../core/theme/commonConsts';
|
|
3
|
-
|
|
4
|
-
export function getCurrentOrientation(forceOnPhone?: boolean): OrientationType {
|
|
5
|
-
if (isTablet || forceOnPhone) {
|
|
6
|
-
let result = Orientation.getInitialOrientation();
|
|
7
|
-
Orientation.getOrientation(orientation => {
|
|
8
|
-
if (orientation != null) {
|
|
9
|
-
result = orientation;
|
|
10
|
-
}
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
return result;
|
|
14
|
-
} else {
|
|
15
|
-
return 'PORTRAIT' as OrientationType;
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function setDefaultOrientation(): void {
|
|
20
|
-
if (isTablet) {
|
|
21
|
-
Orientation.unlockAllOrientations();
|
|
22
|
-
} else {
|
|
23
|
-
Orientation.lockToPortrait();
|
|
24
|
-
}
|
|
25
|
-
}
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import {Linking} from 'react-native';
|
|
2
|
-
import Share, {ShareOptions} from 'react-native-share';
|
|
3
|
-
import {isAndroid, isIos} from '../../core/theme/commonConsts';
|
|
4
|
-
import {
|
|
5
|
-
ShareOpenResult,
|
|
6
|
-
ShareSingleOptions,
|
|
7
|
-
ShareSingleResult,
|
|
8
|
-
} from 'react-native-share/lib/typescript/src/types';
|
|
9
|
-
|
|
10
|
-
export async function showShareDialog(
|
|
11
|
-
options: ShareOptions,
|
|
12
|
-
completedCallback?: (result: ShareOpenResult) => void,
|
|
13
|
-
errorCallback?: (error: Error | unknown) => void,
|
|
14
|
-
) {
|
|
15
|
-
try {
|
|
16
|
-
const result = await Share.open(options);
|
|
17
|
-
completedCallback?.(result);
|
|
18
|
-
} catch (error: Error | unknown) {
|
|
19
|
-
errorCallback?.(error);
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export async function showShareSocialDialog(
|
|
24
|
-
options: ShareSingleOptions,
|
|
25
|
-
completedCallback?: (result: ShareSingleResult) => void,
|
|
26
|
-
errorCallback?: (error: Error | unknown) => void,
|
|
27
|
-
) {
|
|
28
|
-
try {
|
|
29
|
-
const result = await Share.shareSingle(options);
|
|
30
|
-
completedCallback?.(result);
|
|
31
|
-
} catch (error: Error | unknown) {
|
|
32
|
-
errorCallback?.(error);
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export async function isPackageInstalled(
|
|
37
|
-
androidPackageName?: string,
|
|
38
|
-
iosUrl?: string,
|
|
39
|
-
): Promise<boolean> {
|
|
40
|
-
if (isAndroid && androidPackageName) {
|
|
41
|
-
return (await Share.isPackageInstalled(androidPackageName)).isInstalled;
|
|
42
|
-
} else if (isIos && iosUrl) {
|
|
43
|
-
return Linking.canOpenURL(iosUrl);
|
|
44
|
-
} else {
|
|
45
|
-
throw new Error('No arguments were given or the platform is not supported');
|
|
46
|
-
}
|
|
47
|
-
}
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
Service,
|
|
3
|
-
TierEntity,
|
|
4
|
-
Percent1,
|
|
5
|
-
} from '../../core/store/Services/servicesState';
|
|
6
|
-
|
|
7
|
-
export function initFees(service: Service, amount: string) {
|
|
8
|
-
let fee = 0;
|
|
9
|
-
if (service?.Fees) {
|
|
10
|
-
for (const f of service.Fees) {
|
|
11
|
-
if (f.AcctType && f.AcctType !== 'SDA') continue;
|
|
12
|
-
fee = fee + calculateFees(Number(amount), f.Tier as TierEntity[]);
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
return fee;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export function calculateFees(amount: number, tiers: TierEntity[]) {
|
|
19
|
-
if (!tiers || !amount || amount < 0) return 0;
|
|
20
|
-
|
|
21
|
-
const tier = tiers.find(t => amount >= t.LowerAmt && amount <= t.UpperAmt);
|
|
22
|
-
|
|
23
|
-
if (!tier) return 0;
|
|
24
|
-
|
|
25
|
-
const fixed = tier.FixedAmt?.Amt || 0;
|
|
26
|
-
const percent = tier.Percent?.Value ? tier.Percent.Value / 100 : 0;
|
|
27
|
-
const minPercentFee = (tier.Percent as Percent1)?.MinAmt || 0;
|
|
28
|
-
const maxPercentFee = tier.Percent?.MaxAmt || 0;
|
|
29
|
-
const calculatedPercentFee = Number((percent * amount).toFixed(2));
|
|
30
|
-
|
|
31
|
-
const percentFee = Math.min(
|
|
32
|
-
Math.max(calculatedPercentFee, minPercentFee),
|
|
33
|
-
maxPercentFee,
|
|
34
|
-
);
|
|
35
|
-
|
|
36
|
-
return fixed + percentFee;
|
|
37
|
-
}
|
|
@@ -1,161 +0,0 @@
|
|
|
1
|
-
import moment from 'moment';
|
|
2
|
-
import {Service} from '../../core/store/Services/servicesState';
|
|
3
|
-
import {ServiceProvider} from '../../core/store/Providers/providersState';
|
|
4
|
-
|
|
5
|
-
const localization = {
|
|
6
|
-
serialNumber: 'الرقم التسلسلي',
|
|
7
|
-
serviceName: 'اسم الخدمة',
|
|
8
|
-
providerName: 'اسم المقدم',
|
|
9
|
-
customerNumber: 'رقم العميل',
|
|
10
|
-
value: 'القيمة',
|
|
11
|
-
serviceCost: 'تكلفة الخدمة',
|
|
12
|
-
operationNumber: 'رقم العملية',
|
|
13
|
-
referenceNumber: 'الرقم المرجعي',
|
|
14
|
-
time: 'الوقت',
|
|
15
|
-
total: 'الإجمالي',
|
|
16
|
-
expiryDate: 'تاريخ الانتهاء',
|
|
17
|
-
dueDate: 'الموعد المستحق',
|
|
18
|
-
issueDate: 'تاريخ الإصدار',
|
|
19
|
-
quantity: 'الكمية',
|
|
20
|
-
vatValue: 'القسيمة',
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
export const makePrintData = ({
|
|
24
|
-
service,
|
|
25
|
-
BillingAcct,
|
|
26
|
-
provider,
|
|
27
|
-
billInfo,
|
|
28
|
-
amuont,
|
|
29
|
-
fees,
|
|
30
|
-
quantity,
|
|
31
|
-
vatValue,
|
|
32
|
-
}: {
|
|
33
|
-
service: Service;
|
|
34
|
-
BillingAcct: string;
|
|
35
|
-
provider: ServiceProvider;
|
|
36
|
-
billInfo: any;
|
|
37
|
-
amuont: string;
|
|
38
|
-
fees: number;
|
|
39
|
-
quantity: string;
|
|
40
|
-
vatValue: string;
|
|
41
|
-
}) => {
|
|
42
|
-
let newData = [];
|
|
43
|
-
newData.push({
|
|
44
|
-
value: 'logo',
|
|
45
|
-
type: 'image',
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
newData.push({
|
|
49
|
-
value: service?.Name,
|
|
50
|
-
type: 'header2',
|
|
51
|
-
});
|
|
52
|
-
if (billInfo?.BillLabel) {
|
|
53
|
-
newData.push({
|
|
54
|
-
value: billInfo?.BillLabel,
|
|
55
|
-
type: 'header2',
|
|
56
|
-
});
|
|
57
|
-
}
|
|
58
|
-
newData.push({
|
|
59
|
-
value: provider?.BillerName,
|
|
60
|
-
type: 'header2',
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
if (BillingAcct) {
|
|
64
|
-
newData.push({
|
|
65
|
-
value: BillingAcct,
|
|
66
|
-
type: 'body',
|
|
67
|
-
label: localization['customerNumber'],
|
|
68
|
-
});
|
|
69
|
-
}
|
|
70
|
-
// response?.PmtInfo?.ExtraBillingAcctKeys?.ExtraBillingAcctKey?.forEach(
|
|
71
|
-
// (item: any) => {
|
|
72
|
-
// newData.push({
|
|
73
|
-
// value: item.Value,
|
|
74
|
-
// type: 'extra_description'
|
|
75
|
-
// });
|
|
76
|
-
// }
|
|
77
|
-
// );
|
|
78
|
-
|
|
79
|
-
if (!!vatValue) {
|
|
80
|
-
newData.push({
|
|
81
|
-
value: `${vatValue}`,
|
|
82
|
-
label: localization['vatValue'],
|
|
83
|
-
type: 'body',
|
|
84
|
-
});
|
|
85
|
-
}
|
|
86
|
-
if (quantity) {
|
|
87
|
-
newData.push({
|
|
88
|
-
value: `${quantity}`,
|
|
89
|
-
label: localization['quantity'],
|
|
90
|
-
type: 'body',
|
|
91
|
-
});
|
|
92
|
-
}
|
|
93
|
-
newData.push({
|
|
94
|
-
value: `${amuont} EGP`,
|
|
95
|
-
label: localization['value'],
|
|
96
|
-
type: 'body',
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
if (fees?.toString() !== '0') {
|
|
100
|
-
newData.push({
|
|
101
|
-
value: `${fees?.toString()} EGP`,
|
|
102
|
-
label: localization['serviceCost'],
|
|
103
|
-
type: 'body',
|
|
104
|
-
});
|
|
105
|
-
}
|
|
106
|
-
newData.push({
|
|
107
|
-
value: moment().format('YYYY-MM-DD HH:mm'),
|
|
108
|
-
label: localization['time'],
|
|
109
|
-
type: 'body',
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
newData.push({
|
|
113
|
-
value: quantity
|
|
114
|
-
? `${(Number(quantity) * (Number(amuont) + Number(fees)))?.toFixed(
|
|
115
|
-
2,
|
|
116
|
-
)} EGP`
|
|
117
|
-
: `${(Number(amuont) + Number(fees))?.toString()} EGP`,
|
|
118
|
-
label: localization['total'],
|
|
119
|
-
type: 'body_title',
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
if (billInfo?.DueDt) {
|
|
123
|
-
newData.push({
|
|
124
|
-
value: billInfo?.DueDt,
|
|
125
|
-
type: 'body',
|
|
126
|
-
label: localization['dueDate'],
|
|
127
|
-
});
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
if (billInfo?.IssueDt) {
|
|
131
|
-
newData.push({
|
|
132
|
-
value: billInfo?.IssueDt,
|
|
133
|
-
type: 'body',
|
|
134
|
-
label: localization['issueDate'],
|
|
135
|
-
});
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
if (billInfo?.ExtraBillInfo) {
|
|
139
|
-
newData.push({
|
|
140
|
-
value: billInfo?.ExtraBillInfo,
|
|
141
|
-
type: 'description',
|
|
142
|
-
label: '',
|
|
143
|
-
});
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
if (!!service?.meta?.note) {
|
|
147
|
-
newData.push({
|
|
148
|
-
value: service?.meta?.note,
|
|
149
|
-
type: 'description',
|
|
150
|
-
});
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
if (!!service?.meta?.explain) {
|
|
154
|
-
newData.push({
|
|
155
|
-
value: service?.meta?.explain,
|
|
156
|
-
type: 'description',
|
|
157
|
-
});
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
return newData;
|
|
161
|
-
};
|
|
@@ -1,229 +0,0 @@
|
|
|
1
|
-
import React, {useState} from 'react';
|
|
2
|
-
import {View, TextInput, Text, StyleSheet} from 'react-native';
|
|
3
|
-
import {
|
|
4
|
-
regexValidation,
|
|
5
|
-
patterns,
|
|
6
|
-
ValidationResult,
|
|
7
|
-
requiredValidation,
|
|
8
|
-
runValidations,
|
|
9
|
-
} from '../regexValidator';
|
|
10
|
-
|
|
11
|
-
interface TextInputWithValidationProps {
|
|
12
|
-
label: string;
|
|
13
|
-
placeholder?: string;
|
|
14
|
-
value: string;
|
|
15
|
-
onChangeText: (text: string) => void;
|
|
16
|
-
secureTextEntry?: boolean;
|
|
17
|
-
validationType?:
|
|
18
|
-
| 'email'
|
|
19
|
-
| 'password'
|
|
20
|
-
| 'phone'
|
|
21
|
-
| 'name'
|
|
22
|
-
| 'username'
|
|
23
|
-
| 'numeric'
|
|
24
|
-
| 'price'
|
|
25
|
-
| 'custom';
|
|
26
|
-
customRegex?: RegExp;
|
|
27
|
-
customErrorMessage?: string;
|
|
28
|
-
required?: boolean;
|
|
29
|
-
minLength?: number;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* A reusable text input component with built-in validation
|
|
34
|
-
*/
|
|
35
|
-
const TextInputWithValidation: React.FC<TextInputWithValidationProps> = ({
|
|
36
|
-
label,
|
|
37
|
-
placeholder,
|
|
38
|
-
value,
|
|
39
|
-
onChangeText,
|
|
40
|
-
secureTextEntry = false,
|
|
41
|
-
validationType = 'custom',
|
|
42
|
-
customRegex,
|
|
43
|
-
customErrorMessage,
|
|
44
|
-
required = false,
|
|
45
|
-
minLength,
|
|
46
|
-
}) => {
|
|
47
|
-
const [touched, setTouched] = useState(false);
|
|
48
|
-
const [validationResult, setValidationResult] = useState<ValidationResult>({
|
|
49
|
-
isValid: true,
|
|
50
|
-
message: '',
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
// Get the appropriate regex based on validation type
|
|
54
|
-
const getRegexForType = (): RegExp => {
|
|
55
|
-
switch (validationType) {
|
|
56
|
-
case 'email':
|
|
57
|
-
return patterns.EMAIL;
|
|
58
|
-
case 'password':
|
|
59
|
-
return patterns.PASSWORD;
|
|
60
|
-
case 'phone':
|
|
61
|
-
return patterns.PHONE;
|
|
62
|
-
case 'name':
|
|
63
|
-
return patterns.NAME;
|
|
64
|
-
case 'username':
|
|
65
|
-
return patterns.USERNAME;
|
|
66
|
-
case 'numeric':
|
|
67
|
-
return patterns.NUMERIC;
|
|
68
|
-
case 'price':
|
|
69
|
-
return patterns.PRICE;
|
|
70
|
-
case 'custom':
|
|
71
|
-
return customRegex || /^.*$/; // Allow anything if no custom regex
|
|
72
|
-
default:
|
|
73
|
-
return /^.*$/;
|
|
74
|
-
}
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
// Get the default error message based on validation type
|
|
78
|
-
const getDefaultErrorMessage = (): string => {
|
|
79
|
-
switch (validationType) {
|
|
80
|
-
case 'email':
|
|
81
|
-
return 'Please enter a valid email address';
|
|
82
|
-
case 'password':
|
|
83
|
-
return 'Password must be at least 8 characters with uppercase, lowercase, and number';
|
|
84
|
-
case 'phone':
|
|
85
|
-
return 'Please enter a valid phone number';
|
|
86
|
-
case 'name':
|
|
87
|
-
return 'Please enter a valid name';
|
|
88
|
-
case 'username':
|
|
89
|
-
return 'Username must be 3-20 characters (letters, numbers, underscores)';
|
|
90
|
-
case 'numeric':
|
|
91
|
-
return 'Please enter numbers only';
|
|
92
|
-
case 'price':
|
|
93
|
-
return 'Please enter a valid price (e.g., 10.99)';
|
|
94
|
-
default:
|
|
95
|
-
return customErrorMessage || 'Invalid input';
|
|
96
|
-
}
|
|
97
|
-
};
|
|
98
|
-
|
|
99
|
-
// Validate the input when it loses focus
|
|
100
|
-
const handleBlur = () => {
|
|
101
|
-
setTouched(true);
|
|
102
|
-
|
|
103
|
-
// Build validations array
|
|
104
|
-
const validations = [];
|
|
105
|
-
|
|
106
|
-
// Add required validation if needed
|
|
107
|
-
if (required) {
|
|
108
|
-
validations.push((val: string) =>
|
|
109
|
-
requiredValidation(val, 'This field is required'),
|
|
110
|
-
);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// Add pattern validation if it's not empty
|
|
114
|
-
validations.push((val: string) => {
|
|
115
|
-
// Skip empty validation if not required
|
|
116
|
-
if (!required && (!val || val.trim() === '')) {
|
|
117
|
-
return {isValid: true, message: ''};
|
|
118
|
-
}
|
|
119
|
-
return regexValidation(val, getRegexForType(), getDefaultErrorMessage());
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
// Run all validations
|
|
123
|
-
const result = runValidations(value, validations);
|
|
124
|
-
setValidationResult(result);
|
|
125
|
-
};
|
|
126
|
-
|
|
127
|
-
return (
|
|
128
|
-
<View style={styles.container}>
|
|
129
|
-
<Text style={styles.label}>{label}</Text>
|
|
130
|
-
<TextInput
|
|
131
|
-
style={[
|
|
132
|
-
styles.input,
|
|
133
|
-
touched && !validationResult.isValid && styles.inputError,
|
|
134
|
-
]}
|
|
135
|
-
placeholder={placeholder}
|
|
136
|
-
value={value}
|
|
137
|
-
onChangeText={onChangeText}
|
|
138
|
-
onBlur={handleBlur}
|
|
139
|
-
secureTextEntry={secureTextEntry}
|
|
140
|
-
/>
|
|
141
|
-
{touched && !validationResult.isValid && (
|
|
142
|
-
<Text style={styles.errorText}>{validationResult.message}</Text>
|
|
143
|
-
)}
|
|
144
|
-
</View>
|
|
145
|
-
);
|
|
146
|
-
};
|
|
147
|
-
|
|
148
|
-
const styles = StyleSheet.create({
|
|
149
|
-
container: {
|
|
150
|
-
marginBottom: 16,
|
|
151
|
-
},
|
|
152
|
-
label: {
|
|
153
|
-
fontSize: 16,
|
|
154
|
-
marginBottom: 8,
|
|
155
|
-
fontWeight: '500',
|
|
156
|
-
},
|
|
157
|
-
input: {
|
|
158
|
-
borderWidth: 1,
|
|
159
|
-
borderColor: '#ccc',
|
|
160
|
-
borderRadius: 4,
|
|
161
|
-
paddingHorizontal: 12,
|
|
162
|
-
paddingVertical: 8,
|
|
163
|
-
fontSize: 16,
|
|
164
|
-
},
|
|
165
|
-
inputError: {
|
|
166
|
-
borderColor: '#ff3b30',
|
|
167
|
-
},
|
|
168
|
-
errorText: {
|
|
169
|
-
color: '#ff3b30',
|
|
170
|
-
fontSize: 14,
|
|
171
|
-
marginTop: 4,
|
|
172
|
-
},
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
export default TextInputWithValidation;
|
|
176
|
-
|
|
177
|
-
// Usage Example:
|
|
178
|
-
/*
|
|
179
|
-
import { TextInputWithValidation } from 'src/common/validations/examples';
|
|
180
|
-
|
|
181
|
-
const MyForm = () => {
|
|
182
|
-
const [email, setEmail] = useState('');
|
|
183
|
-
const [password, setPassword] = useState('');
|
|
184
|
-
const [phone, setPhone] = useState('');
|
|
185
|
-
const [customField, setCustomField] = useState('');
|
|
186
|
-
|
|
187
|
-
return (
|
|
188
|
-
<View>
|
|
189
|
-
<TextInputWithValidation
|
|
190
|
-
label="Email"
|
|
191
|
-
placeholder="Enter your email"
|
|
192
|
-
value={email}
|
|
193
|
-
onChangeText={setEmail}
|
|
194
|
-
validationType="email"
|
|
195
|
-
required
|
|
196
|
-
/>
|
|
197
|
-
|
|
198
|
-
<TextInputWithValidation
|
|
199
|
-
label="Password"
|
|
200
|
-
placeholder="Enter your password"
|
|
201
|
-
value={password}
|
|
202
|
-
onChangeText={setPassword}
|
|
203
|
-
validationType="password"
|
|
204
|
-
required
|
|
205
|
-
secureTextEntry
|
|
206
|
-
/>
|
|
207
|
-
|
|
208
|
-
<TextInputWithValidation
|
|
209
|
-
label="Phone Number"
|
|
210
|
-
placeholder="Enter your phone number"
|
|
211
|
-
value={phone}
|
|
212
|
-
onChangeText={setPhone}
|
|
213
|
-
validationType="phone"
|
|
214
|
-
/>
|
|
215
|
-
|
|
216
|
-
<TextInputWithValidation
|
|
217
|
-
label="Product ID"
|
|
218
|
-
placeholder="Enter product ID"
|
|
219
|
-
value={customField}
|
|
220
|
-
onChangeText={setCustomField}
|
|
221
|
-
validationType="custom"
|
|
222
|
-
customRegex={/^PRD-[0-9]{6}$/}
|
|
223
|
-
customErrorMessage="Product ID must be in format PRD-123456"
|
|
224
|
-
required
|
|
225
|
-
/>
|
|
226
|
-
</View>
|
|
227
|
-
);
|
|
228
|
-
};
|
|
229
|
-
*/
|