@react-native-ohos/react-native-credit-card-input 1.0.1-rc.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.
Files changed (66) hide show
  1. package/LICENSE +20 -0
  2. package/README.OpenSource +11 -0
  3. package/README.md +9 -0
  4. package/lib/commonjs/CreditCardInput.js +138 -0
  5. package/lib/commonjs/CreditCardInput.js.map +1 -0
  6. package/lib/commonjs/CreditCardView.js +175 -0
  7. package/lib/commonjs/CreditCardView.js.map +1 -0
  8. package/lib/commonjs/Icons.js +17 -0
  9. package/lib/commonjs/Icons.js.map +1 -0
  10. package/lib/commonjs/LiteCreditCardInput.js +189 -0
  11. package/lib/commonjs/LiteCreditCardInput.js.map +1 -0
  12. package/lib/commonjs/index.js +35 -0
  13. package/lib/commonjs/index.js.map +1 -0
  14. package/lib/commonjs/package.json +1 -0
  15. package/lib/commonjs/useCreditCardForm.js +95 -0
  16. package/lib/commonjs/useCreditCardForm.js.map +1 -0
  17. package/lib/module/CreditCardInput.js +134 -0
  18. package/lib/module/CreditCardInput.js.map +1 -0
  19. package/lib/module/CreditCardView.js +170 -0
  20. package/lib/module/CreditCardView.js.map +1 -0
  21. package/lib/module/Icons.js +13 -0
  22. package/lib/module/Icons.js.map +1 -0
  23. package/lib/module/LiteCreditCardInput.js +184 -0
  24. package/lib/module/LiteCreditCardInput.js.map +1 -0
  25. package/lib/module/index.js +7 -0
  26. package/lib/module/index.js.map +1 -0
  27. package/lib/module/package.json +1 -0
  28. package/lib/module/useCreditCardForm.js +89 -0
  29. package/lib/module/useCreditCardForm.js.map +1 -0
  30. package/lib/typescript/commonjs/jest.setup.d.ts +2 -0
  31. package/lib/typescript/commonjs/jest.setup.d.ts.map +1 -0
  32. package/lib/typescript/commonjs/package.json +1 -0
  33. package/lib/typescript/commonjs/src/CreditCardInput.d.ts +25 -0
  34. package/lib/typescript/commonjs/src/CreditCardInput.d.ts.map +1 -0
  35. package/lib/typescript/commonjs/src/CreditCardView.d.ts +23 -0
  36. package/lib/typescript/commonjs/src/CreditCardView.d.ts.map +1 -0
  37. package/lib/typescript/commonjs/src/Icons.d.ts +11 -0
  38. package/lib/typescript/commonjs/src/Icons.d.ts.map +1 -0
  39. package/lib/typescript/commonjs/src/LiteCreditCardInput.d.ts +19 -0
  40. package/lib/typescript/commonjs/src/LiteCreditCardInput.d.ts.map +1 -0
  41. package/lib/typescript/commonjs/src/index.d.ts +5 -0
  42. package/lib/typescript/commonjs/src/index.d.ts.map +1 -0
  43. package/lib/typescript/commonjs/src/useCreditCardForm.d.ts +25 -0
  44. package/lib/typescript/commonjs/src/useCreditCardForm.d.ts.map +1 -0
  45. package/lib/typescript/module/jest.setup.d.ts +2 -0
  46. package/lib/typescript/module/jest.setup.d.ts.map +1 -0
  47. package/lib/typescript/module/package.json +1 -0
  48. package/lib/typescript/module/src/CreditCardInput.d.ts +25 -0
  49. package/lib/typescript/module/src/CreditCardInput.d.ts.map +1 -0
  50. package/lib/typescript/module/src/CreditCardView.d.ts +23 -0
  51. package/lib/typescript/module/src/CreditCardView.d.ts.map +1 -0
  52. package/lib/typescript/module/src/Icons.d.ts +11 -0
  53. package/lib/typescript/module/src/Icons.d.ts.map +1 -0
  54. package/lib/typescript/module/src/LiteCreditCardInput.d.ts +19 -0
  55. package/lib/typescript/module/src/LiteCreditCardInput.d.ts.map +1 -0
  56. package/lib/typescript/module/src/index.d.ts +5 -0
  57. package/lib/typescript/module/src/index.d.ts.map +1 -0
  58. package/lib/typescript/module/src/useCreditCardForm.d.ts +25 -0
  59. package/lib/typescript/module/src/useCreditCardForm.d.ts.map +1 -0
  60. package/package.json +145 -0
  61. package/src/CreditCardInput.tsx +163 -0
  62. package/src/CreditCardView.tsx +248 -0
  63. package/src/Icons.ts +18 -0
  64. package/src/LiteCreditCardInput.tsx +226 -0
  65. package/src/index.tsx +12 -0
  66. package/src/useCreditCardForm.tsx +161 -0
@@ -0,0 +1,226 @@
1
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
+ import {
3
+ Image,
4
+ LayoutAnimation,
5
+ StyleSheet,
6
+ TextInput,
7
+ TouchableOpacity,
8
+ View,
9
+ type TextStyle,
10
+ type ViewStyle,
11
+ } from 'react-native';
12
+ import Icons from './Icons';
13
+ import {
14
+ useCreditCardForm,
15
+ type CreditCardFormData,
16
+ type CreditCardFormField,
17
+ } from './useCreditCardForm';
18
+
19
+ interface Props {
20
+ autoFocus?: boolean;
21
+ style?: ViewStyle;
22
+ inputStyle?: TextStyle;
23
+ placeholderColor?: string;
24
+ placeholders?: {
25
+ number: string;
26
+ expiry: string;
27
+ cvc: string;
28
+ };
29
+ onChange?: (formData: CreditCardFormData) => void;
30
+ onFocusField?: (field: CreditCardFormField) => void;
31
+ testID?: string;
32
+ }
33
+
34
+ const s = StyleSheet.create({
35
+ container: {
36
+ paddingVertical: 10,
37
+ paddingHorizontal: 15,
38
+ flexDirection: 'row',
39
+ alignItems: 'center',
40
+ overflow: 'hidden',
41
+ },
42
+ icon: {
43
+ width: 48,
44
+ height: 40,
45
+ resizeMode: 'contain',
46
+ },
47
+ expanded: {
48
+ flex: 1,
49
+ },
50
+ hidden: {
51
+ width: 0,
52
+ },
53
+ leftPart: {
54
+ overflow: 'hidden',
55
+ },
56
+ rightPart: {
57
+ overflow: 'hidden',
58
+ flexDirection: 'row',
59
+ },
60
+ last4: {
61
+ flex: 1,
62
+ justifyContent: 'center',
63
+ },
64
+ numberInput: {
65
+ width: 1000,
66
+ },
67
+ expiryInput: {
68
+ width: 80,
69
+ },
70
+ cvcInput: {
71
+ width: 80,
72
+ },
73
+ last4Input: {
74
+ width: 60,
75
+ marginLeft: 20,
76
+ },
77
+ input: {
78
+ height: 40,
79
+ fontSize: 16,
80
+ // // @ts-expect-error outlineWidth is used to hide the text-input outline on react-native-web
81
+ outlineWidth: 0,
82
+ },
83
+ });
84
+
85
+ const LiteCreditCardInput = (props: Props) => {
86
+ const {
87
+ autoFocus = false,
88
+ style,
89
+ inputStyle,
90
+ placeholderColor = 'darkgray',
91
+ placeholders = {
92
+ number: '1234 5678 1234 5678',
93
+ expiry: 'MM/YY',
94
+ cvc: 'CVC',
95
+ },
96
+ onChange = () => {},
97
+ onFocusField = () => {},
98
+ testID,
99
+ } = props;
100
+
101
+ const _onChange = (formData: CreditCardFormData): void => {
102
+ // Focus next field when number/expiry field become valid
103
+ if (status.number !== 'valid' && formData.status.number === 'valid') {
104
+ toggleFormState();
105
+ expiryInput.current?.focus();
106
+ }
107
+
108
+ if (status.expiry !== 'valid' && formData.status.expiry === 'valid') {
109
+ cvcInput.current?.focus();
110
+ }
111
+
112
+ onChange(formData);
113
+ };
114
+
115
+ const { values, status, onChangeValue } = useCreditCardForm(_onChange);
116
+
117
+ const [showRightPart, setShowRightPart] = useState(false);
118
+
119
+ const toggleFormState = useCallback(() => {
120
+ LayoutAnimation.easeInEaseOut();
121
+ setShowRightPart((v) => !v);
122
+ }, []);
123
+
124
+ const numberInput = useRef<TextInput>(null);
125
+ const expiryInput = useRef<TextInput>(null);
126
+ const cvcInput = useRef<TextInput>(null);
127
+
128
+ useEffect(() => {
129
+ if (autoFocus) numberInput.current?.focus();
130
+ }, [autoFocus]);
131
+
132
+ const cardIcon = useMemo(() => {
133
+ if (values.type && Icons[values.type]) return Icons[values.type];
134
+ return Icons.placeholder;
135
+ }, [values.type]);
136
+
137
+ return (
138
+ <View
139
+ style={[s.container, style]}
140
+ testID={testID}
141
+ >
142
+ <View style={[s.leftPart, showRightPart ? s.hidden : s.expanded]}>
143
+ <View style={[s.numberInput]}>
144
+ <TextInput
145
+ ref={numberInput}
146
+ keyboardType="default"
147
+ style={[s.input, inputStyle]}
148
+ placeholderTextColor={placeholderColor}
149
+ placeholder={placeholders.number}
150
+ value={values.number}
151
+ onChangeText={(v) => onChangeValue('number', v)}
152
+ onFocus={() => onFocusField('number')}
153
+ autoCorrect={false}
154
+ underlineColorAndroid={'transparent'}
155
+ testID="CC_NUMBER"
156
+ />
157
+ </View>
158
+ </View>
159
+
160
+ <TouchableOpacity
161
+ activeOpacity={0.8}
162
+ onPress={toggleFormState}
163
+ >
164
+ <Image
165
+ style={s.icon}
166
+ source={{ uri: cardIcon }}
167
+ />
168
+ </TouchableOpacity>
169
+
170
+ <View style={[s.rightPart, showRightPart ? s.expanded : s.hidden]}>
171
+ <TouchableOpacity
172
+ activeOpacity={0.8}
173
+ onPress={toggleFormState}
174
+ style={s.last4}
175
+ >
176
+ <View pointerEvents={'none'}>
177
+ <TextInput
178
+ keyboardType="default"
179
+ value={
180
+ status.number === 'valid'
181
+ ? values.number.slice(values.number.length - 4)
182
+ : ''
183
+ }
184
+ style={[s.input, s.last4Input]}
185
+ readOnly
186
+ />
187
+ </View>
188
+ </TouchableOpacity>
189
+
190
+ <View style={s.expiryInput}>
191
+ <TextInput
192
+ ref={expiryInput}
193
+ keyboardType="default"
194
+ style={[s.input, inputStyle]}
195
+ placeholderTextColor={placeholderColor}
196
+ placeholder={placeholders.expiry}
197
+ value={values.expiry}
198
+ onChangeText={(v) => onChangeValue('expiry', v)}
199
+ onFocus={() => onFocusField('expiry')}
200
+ autoCorrect={false}
201
+ underlineColorAndroid={'transparent'}
202
+ testID="CC_EXPIRY"
203
+ />
204
+ </View>
205
+
206
+ <View style={s.cvcInput}>
207
+ <TextInput
208
+ ref={cvcInput}
209
+ keyboardType="default"
210
+ style={[s.input, inputStyle]}
211
+ placeholderTextColor={placeholderColor}
212
+ placeholder={placeholders.cvc}
213
+ value={values.cvc}
214
+ onChangeText={(v) => onChangeValue('cvc', v)}
215
+ onFocus={() => onFocusField('cvc')}
216
+ autoCorrect={false}
217
+ underlineColorAndroid={'transparent'}
218
+ testID="CC_CVC"
219
+ />
220
+ </View>
221
+ </View>
222
+ </View>
223
+ );
224
+ };
225
+
226
+ export default LiteCreditCardInput;
package/src/index.tsx ADDED
@@ -0,0 +1,12 @@
1
+ export { default as CreditCardView } from './CreditCardView';
2
+ export { default as CreditCardInput } from './CreditCardInput';
3
+ export { default as LiteCreditCardInput } from './LiteCreditCardInput';
4
+
5
+ export {
6
+ type CreditCardFormField,
7
+ type CreditCardFormValues,
8
+ type ValidationState,
9
+ type CreditCardFormState,
10
+ type CreditCardFormData,
11
+ useCreditCardForm,
12
+ } from './useCreditCardForm';
@@ -0,0 +1,161 @@
1
+ import { useCallback, useState } from 'react';
2
+ import cardValidator from 'card-validator';
3
+
4
+ export type CreditCardIssuer =
5
+ | 'visa'
6
+ | 'mastercard'
7
+ | 'american-express'
8
+ | 'diners-club'
9
+ | 'discover'
10
+ | 'jcb';
11
+
12
+ export type CreditCardFormField = 'number' | 'expiry' | 'cvc';
13
+
14
+ export type CreditCardFormValues = {
15
+ number: string;
16
+ expiry: string;
17
+ cvc: string;
18
+ type?: CreditCardIssuer;
19
+ };
20
+
21
+ export type ValidationState = 'incomplete' | 'invalid' | 'valid';
22
+
23
+ export type CreditCardFormState = {
24
+ number: ValidationState;
25
+ expiry: ValidationState;
26
+ cvc: ValidationState;
27
+ };
28
+
29
+ export type CreditCardFormData = {
30
+ valid: boolean;
31
+ values: CreditCardFormValues;
32
+ status: CreditCardFormState;
33
+ };
34
+
35
+ // --- Utilities
36
+
37
+ const toStatus = (validation: {
38
+ isValid: boolean;
39
+ isPotentiallyValid: boolean;
40
+ }): ValidationState => {
41
+ return validation.isValid
42
+ ? 'valid'
43
+ : validation.isPotentiallyValid
44
+ ? 'incomplete'
45
+ : 'invalid';
46
+ };
47
+
48
+ const removeNonNumber = (string = '') => string.replace(/[^\d]/g, '');
49
+
50
+ const limitLength = (string = '', maxLength: number) =>
51
+ string.slice(0, maxLength);
52
+
53
+ const addGaps = (string = '', gaps: number[]) => {
54
+ const offsets = [0].concat(gaps).concat([string.length]);
55
+
56
+ return offsets
57
+ .map((end, index) => {
58
+ if (index === 0) return '';
59
+ const start = offsets[index - 1] || 0;
60
+ return string.slice(start, end);
61
+ })
62
+ .filter((part) => part !== '')
63
+ .join(' ');
64
+ };
65
+
66
+ const formatCardNumber = (
67
+ number: string,
68
+ maxLength: number,
69
+ gaps: number[]
70
+ ) => {
71
+ const numberSanitized = removeNonNumber(number);
72
+ const lengthSanitized = limitLength(numberSanitized, maxLength);
73
+ const formatted = addGaps(lengthSanitized, gaps);
74
+ return formatted;
75
+ };
76
+
77
+ const formatCardExpiry = (expiry: string) => {
78
+ const sanitized = limitLength(removeNonNumber(expiry), 4);
79
+ if (sanitized.match(/^[2-9]$/)) {
80
+ return `0${sanitized}`;
81
+ }
82
+ if (sanitized.length > 2) {
83
+ return `${sanitized.substr(0, 2)}/${sanitized.substr(2, sanitized.length)}`;
84
+ }
85
+ return sanitized;
86
+ };
87
+
88
+ const formatCardCVC = (cvc: string, cvcMaxLength: number) => {
89
+ return limitLength(removeNonNumber(cvc), cvcMaxLength);
90
+ };
91
+
92
+ export const useCreditCardForm = (
93
+ onChange: (formData: CreditCardFormData) => void
94
+ ) => {
95
+ const [formState, setFormState] = useState<CreditCardFormState>({
96
+ number: 'incomplete',
97
+ expiry: 'incomplete',
98
+ cvc: 'incomplete',
99
+ });
100
+
101
+ const [values, setValues] = useState<CreditCardFormValues>({
102
+ number: '',
103
+ expiry: '',
104
+ cvc: '',
105
+ type: undefined,
106
+ });
107
+
108
+ const onChangeValue = useCallback(
109
+ (field: CreditCardFormField, value: string) => {
110
+ const newValues = {
111
+ ...values,
112
+ [field]: value,
113
+ };
114
+
115
+ const numberValidation = cardValidator.number(newValues.number);
116
+
117
+ // When card issuer cant be detected, use these default (3 digit CVC, 16 digit card number with spaces every 4 digit)
118
+ const cvcMaxLength = numberValidation.card?.code.size || 3;
119
+ const cardNumberGaps = numberValidation.card?.gaps || [4, 8, 12];
120
+ const cardNumberMaxLength =
121
+ // Credit card number can vary. Use the longest possible as maximum (otherwise fallback to 16)
122
+ Math.max(...(numberValidation.card?.lengths || [16]));
123
+
124
+ const newFormattedValues = {
125
+ number: formatCardNumber(
126
+ newValues.number,
127
+ cardNumberMaxLength,
128
+ cardNumberGaps
129
+ ),
130
+ expiry: formatCardExpiry(newValues.expiry),
131
+ cvc: formatCardCVC(newValues.cvc, cvcMaxLength),
132
+ type: numberValidation.card?.type as CreditCardIssuer,
133
+ };
134
+
135
+ const newFormState = {
136
+ number: toStatus(cardValidator.number(newFormattedValues.number)),
137
+ expiry: toStatus(cardValidator.expirationDate(newFormattedValues.expiry)),
138
+ cvc: toStatus(cardValidator.cvv(newFormattedValues.cvc, cvcMaxLength)),
139
+ };
140
+
141
+ setValues(newFormattedValues);
142
+ setFormState(newFormState);
143
+
144
+ onChange({
145
+ valid:
146
+ newFormState.number === 'valid' &&
147
+ newFormState.expiry === 'valid' &&
148
+ newFormState.cvc === 'valid',
149
+ values: newFormattedValues,
150
+ status: newFormState,
151
+ });
152
+ },
153
+ [values, onChange]
154
+ );
155
+
156
+ return {
157
+ values,
158
+ status: formState,
159
+ onChangeValue,
160
+ };
161
+ };