@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.
- package/LICENSE +20 -0
- package/README.OpenSource +11 -0
- package/README.md +9 -0
- package/lib/commonjs/CreditCardInput.js +138 -0
- package/lib/commonjs/CreditCardInput.js.map +1 -0
- package/lib/commonjs/CreditCardView.js +175 -0
- package/lib/commonjs/CreditCardView.js.map +1 -0
- package/lib/commonjs/Icons.js +17 -0
- package/lib/commonjs/Icons.js.map +1 -0
- package/lib/commonjs/LiteCreditCardInput.js +189 -0
- package/lib/commonjs/LiteCreditCardInput.js.map +1 -0
- package/lib/commonjs/index.js +35 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/commonjs/package.json +1 -0
- package/lib/commonjs/useCreditCardForm.js +95 -0
- package/lib/commonjs/useCreditCardForm.js.map +1 -0
- package/lib/module/CreditCardInput.js +134 -0
- package/lib/module/CreditCardInput.js.map +1 -0
- package/lib/module/CreditCardView.js +170 -0
- package/lib/module/CreditCardView.js.map +1 -0
- package/lib/module/Icons.js +13 -0
- package/lib/module/Icons.js.map +1 -0
- package/lib/module/LiteCreditCardInput.js +184 -0
- package/lib/module/LiteCreditCardInput.js.map +1 -0
- package/lib/module/index.js +7 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/useCreditCardForm.js +89 -0
- package/lib/module/useCreditCardForm.js.map +1 -0
- package/lib/typescript/commonjs/jest.setup.d.ts +2 -0
- package/lib/typescript/commonjs/jest.setup.d.ts.map +1 -0
- package/lib/typescript/commonjs/package.json +1 -0
- package/lib/typescript/commonjs/src/CreditCardInput.d.ts +25 -0
- package/lib/typescript/commonjs/src/CreditCardInput.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/CreditCardView.d.ts +23 -0
- package/lib/typescript/commonjs/src/CreditCardView.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/Icons.d.ts +11 -0
- package/lib/typescript/commonjs/src/Icons.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/LiteCreditCardInput.d.ts +19 -0
- package/lib/typescript/commonjs/src/LiteCreditCardInput.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/index.d.ts +5 -0
- package/lib/typescript/commonjs/src/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/useCreditCardForm.d.ts +25 -0
- package/lib/typescript/commonjs/src/useCreditCardForm.d.ts.map +1 -0
- package/lib/typescript/module/jest.setup.d.ts +2 -0
- package/lib/typescript/module/jest.setup.d.ts.map +1 -0
- package/lib/typescript/module/package.json +1 -0
- package/lib/typescript/module/src/CreditCardInput.d.ts +25 -0
- package/lib/typescript/module/src/CreditCardInput.d.ts.map +1 -0
- package/lib/typescript/module/src/CreditCardView.d.ts +23 -0
- package/lib/typescript/module/src/CreditCardView.d.ts.map +1 -0
- package/lib/typescript/module/src/Icons.d.ts +11 -0
- package/lib/typescript/module/src/Icons.d.ts.map +1 -0
- package/lib/typescript/module/src/LiteCreditCardInput.d.ts +19 -0
- package/lib/typescript/module/src/LiteCreditCardInput.d.ts.map +1 -0
- package/lib/typescript/module/src/index.d.ts +5 -0
- package/lib/typescript/module/src/index.d.ts.map +1 -0
- package/lib/typescript/module/src/useCreditCardForm.d.ts +25 -0
- package/lib/typescript/module/src/useCreditCardForm.d.ts.map +1 -0
- package/package.json +145 -0
- package/src/CreditCardInput.tsx +163 -0
- package/src/CreditCardView.tsx +248 -0
- package/src/Icons.ts +18 -0
- package/src/LiteCreditCardInput.tsx +226 -0
- package/src/index.tsx +12 -0
- 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
|
+
};
|