@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.
Files changed (138) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +90 -56
  3. package/index.js +4 -0
  4. package/package.json +8 -2
  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 -20
  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 +4 -5
  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
@@ -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
- */