react-native-credit-card-scanner 1.0.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 (40) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +202 -0
  3. package/lib/module/components/CreditCardScanner.js +66 -0
  4. package/lib/module/components/CreditCardScanner.js.map +1 -0
  5. package/lib/module/components/ScannerOverlay.js +43 -0
  6. package/lib/module/components/ScannerOverlay.js.map +1 -0
  7. package/lib/module/hooks/useCardScanner.js +53 -0
  8. package/lib/module/hooks/useCardScanner.js.map +1 -0
  9. package/lib/module/index.js +5 -0
  10. package/lib/module/index.js.map +1 -0
  11. package/lib/module/package.json +1 -0
  12. package/lib/module/types/index.js +4 -0
  13. package/lib/module/types/index.js.map +1 -0
  14. package/lib/module/utils/parsers.js +75 -0
  15. package/lib/module/utils/parsers.js.map +1 -0
  16. package/lib/module/utils/validators.js +65 -0
  17. package/lib/module/utils/validators.js.map +1 -0
  18. package/lib/typescript/package.json +1 -0
  19. package/lib/typescript/src/components/CreditCardScanner.d.ts +4 -0
  20. package/lib/typescript/src/components/CreditCardScanner.d.ts.map +1 -0
  21. package/lib/typescript/src/components/ScannerOverlay.d.ts +9 -0
  22. package/lib/typescript/src/components/ScannerOverlay.d.ts.map +1 -0
  23. package/lib/typescript/src/hooks/useCardScanner.d.ts +11 -0
  24. package/lib/typescript/src/hooks/useCardScanner.d.ts.map +1 -0
  25. package/lib/typescript/src/index.d.ts +3 -0
  26. package/lib/typescript/src/index.d.ts.map +1 -0
  27. package/lib/typescript/src/types/index.d.ts +35 -0
  28. package/lib/typescript/src/types/index.d.ts.map +1 -0
  29. package/lib/typescript/src/utils/parsers.d.ts +6 -0
  30. package/lib/typescript/src/utils/parsers.d.ts.map +1 -0
  31. package/lib/typescript/src/utils/validators.d.ts +4 -0
  32. package/lib/typescript/src/utils/validators.d.ts.map +1 -0
  33. package/package.json +137 -0
  34. package/src/components/CreditCardScanner.tsx +63 -0
  35. package/src/components/ScannerOverlay.tsx +52 -0
  36. package/src/hooks/useCardScanner.ts +56 -0
  37. package/src/index.tsx +2 -0
  38. package/src/types/index.ts +56 -0
  39. package/src/utils/parsers.ts +83 -0
  40. package/src/utils/validators.ts +86 -0
package/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Developer
4
+ Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ of this software and associated documentation files (the "Software"), to deal
6
+ in the Software without restriction, including without limitation the rights
7
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ copies of the Software, and to permit persons to whom the Software is
9
+ furnished to do so, subject to the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be included in all
12
+ copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,202 @@
1
+ # react-native-credit-card-scanner
2
+
3
+ A high-performance credit card scanning library for React Native built on top of Vision Camera.
4
+
5
+ ## Demo
6
+
7
+ https://github.com/user-attachments/assets/0c07269a-17da-410f-aacd-a179115bfac4
8
+
9
+ ## Requirements
10
+
11
+ | Requirement | Minimum version |
12
+ | ----------------------------------- | --------------- |
13
+ | React Native | 0.81 |
14
+ | iOS | 15.1 |
15
+ | Android Minimum SDK | 26 |
16
+ | Android Target SDK | 36 |
17
+ | react-native-vision-camera | 5.0.0 |
18
+ | react-native-vision-camera-ocr-plus | 2.0.0 |
19
+ | react-native-worklets | 0.8.x |
20
+ | Expo (if used) | 54 |
21
+
22
+ ## Installation
23
+
24
+ ### 1. Install the library
25
+
26
+ ```sh
27
+ npm install react-native-credit-card-scanner
28
+ # or
29
+ yarn add react-native-credit-card-scanner
30
+ ```
31
+
32
+ ### 2. Install Peer Dependencies
33
+
34
+ This library relies on several peer dependencies to function properly. You must install them in your project:
35
+
36
+ ```sh
37
+ npm install react-native-vision-camera \
38
+ react-native-vision-camera-ocr-plus \
39
+ react-native-hole-view \
40
+ react-native-vision-camera-worklets \
41
+ react-native-worklets \
42
+ react-native-nitro-modules \
43
+ react-native-nitro-image
44
+ ```
45
+
46
+ ### Peer dependencies
47
+
48
+ | Package | Version |
49
+ | ------------------------------------- | --------- |
50
+ | `react-native-vision-camera` | `>=5.0.0` |
51
+ | `react-native-vision-camera-ocr-plus` | `>=2.0.0` |
52
+ | `react-native-hole-view` | `*` |
53
+ | `react-native-nitro-modules` | `*` |
54
+ | `react-native-nitro-image` | `*` |
55
+ | `react-native-vision-camera-worklets` | `*` |
56
+ | `react-native-worklets` | `>=0.8.0` |
57
+
58
+ ### 3. Add Babel Plugin for Worklets
59
+
60
+ You must add the worklets plugin to your `babel.config.js`:
61
+
62
+ ```javascript
63
+ module.exports = {
64
+ //other options.....
65
+ plugins: [
66
+ // ...other plugins
67
+ ['react-native-worklets/plugin'],
68
+ ],
69
+ };
70
+ ```
71
+
72
+ > **Warning**: Ensure you clear your bundler cache after adding the babel plugin (e.g., `npm start -- --reset-cache` or `yarn start --reset-cache`).
73
+
74
+ ### 4. Setup Permissions
75
+
76
+ #### iOS
77
+
78
+ Add the camera permission to your `ios/YourApp/Info.plist`:
79
+
80
+ ```xml
81
+ <key>NSCameraUsageDescription</key>
82
+ <string>We need access to your camera to scan credit cards.</string>
83
+ ```
84
+
85
+ #### Android
86
+
87
+ Add the camera permission to your `android/app/src/main/AndroidManifest.xml`:
88
+
89
+ ```xml
90
+ <uses-permission android:name="android.permission.CAMERA" />
91
+ ```
92
+
93
+ ### 5. Expo Usage
94
+
95
+ This library works with Expo, **but only with custom Expo Development Builds (Prebuild)** or the Bare Workflow. It **will not work in Expo Go** due to its reliance on custom native code, C++ Nitro Modules, and JSI.
96
+
97
+ If you are using Expo, make sure to follow the specific setup guides for Expo in the official documentation of our core dependencies:
98
+
99
+ - [React Native Vision Camera - Expo Guide](https://react-native-vision-camera.com/docs/guides/getting-started)
100
+ - [React Native Worklets - Getting Started](https://docs.swmansion.com/react-native-worklets/docs/fundamentals/getting-started/)
101
+
102
+ You will typically need to configure the `app.json` config plugins for Vision Camera to automatically handle camera permissions during prebuild.
103
+
104
+ ---
105
+
106
+ ## Available Components
107
+
108
+ ### `CreditCardScanner`
109
+
110
+ The main orchestrator component that handles rendering the camera, drawing the overlay cutout, and processing the OCR output.
111
+
112
+ #### Props
113
+
114
+ | Prop | Type | Description |
115
+ | ------------------ | ---------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
116
+ | `isActive` | `boolean` | Determines if the camera and scanner are actively running. Pass `false` to pause/hide it and save battery. |
117
+ | `onScanSuccess` | `(result: CardResult) => void` | Callback triggered when a valid credit card is successfully scanned and verified. |
118
+ | `cameraRef` | `React.RefObject<CameraRef\|null>` | _(Optional)_ Programatically control camera properties/functions. |
119
+ | `overlayColor` | `string` | _(Optional)_ The color of the dimming mask over the camera (e.g., `rgba(0, 0, 0, 0.7)`). |
120
+ | `holeViewConfig` | `HoleViewConfig` | _(Optional)_ Configuration object specifying the position and size of the clear cutout window where the card should be aligned. Includes `left`, `top`, `width`, and `height`. |
121
+ | `parentViewHeight` | `number` | _(Optional)_ The exact height of the parent container holding the scanner, required for accurate proportional cutout bounds and scanRegion mapping (always pass it if the camera view is not full screen). |
122
+ | `style` | `ViewStyle` | _(Optional)_ Style applied to the underlying container wrapper. |
123
+ | `cameraProps` | `Partial<CameraProps>` | _(Optional)_ Pass vision camera props to <Camera /> component. |
124
+
125
+ #### Types
126
+
127
+ **`CardResult`**
128
+
129
+ ```typescript
130
+ interface CardResult {
131
+ cardNumber: string; // The 13-19 digit card number
132
+ expiryDate: string | null; // e.g., '12/26'
133
+ cardholderName: string | undefined; // Extracted name (if matched)
134
+ issuer: 'Visa' | 'Mastercard' | 'Amex' | 'Discover' | 'Unknown';
135
+ }
136
+ ```
137
+
138
+ ---
139
+
140
+ ## Usage Example
141
+
142
+ ```tsx
143
+ import React, { useState } from 'react';
144
+ import { View, Text, Button } from 'react-native';
145
+ import {
146
+ CreditCardScanner,
147
+ type CardResult,
148
+ } from 'react-native-credit-card-scanner';
149
+ import { useCameraPermission } from 'react-native-vision-camera';
150
+
151
+ export default function App() {
152
+ const [isActive, setIsActive] = useState(false);
153
+ const { hasPermission, requestPermission } = useCameraPermission();
154
+
155
+ const handleScanSuccess = (result: CardResult) => {
156
+ setIsActive(false);
157
+ console.log('Card Scanned:', result.cardNumber);
158
+ };
159
+
160
+ const startScan = () => {
161
+ if (!hasPermission) requestPermission();
162
+ else setIsActive(true);
163
+ };
164
+
165
+ return (
166
+ <View style={{ flex: 1, backgroundColor: '#000' }}>
167
+ {isActive ? (
168
+ <CreditCardScanner
169
+ isActive={isActive}
170
+ onScanSuccess={handleScanSuccess}
171
+ overlayColor="rgba(0, 0, 0, 0.8)"
172
+ />
173
+ ) : (
174
+ <View
175
+ style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}
176
+ >
177
+ <Button title="Scan Credit Card" onPress={startScan} />
178
+ </View>
179
+ )}
180
+ </View>
181
+ );
182
+ }
183
+ ```
184
+
185
+ ## Acknowledgments
186
+
187
+ This library depends on several awesome packages by community, Thanks to the authors of these libraries for making this possible.
188
+
189
+ - [react-native-vision-camera](https://github.com/mrousavy/react-native-vision-camera)
190
+ - [react-native-vision-camera-ocr-plus](https://github.com/jamenamcinteer/react-native-vision-camera-ocr-plus)
191
+ - [react-native-hole-view](https://github.com/ibitcy/react-native-hole-view)
192
+ - [react-native-worklets](https://docs.swmansion.com/react-native-worklets)
193
+
194
+ ## Contributing
195
+
196
+ - [Development workflow](CONTRIBUTING.md#development-workflow)
197
+ - [Sending a pull request](CONTRIBUTING.md#sending-a-pull-request)
198
+ - [Code of conduct](CODE_OF_CONDUCT.md)
199
+
200
+ ## License
201
+
202
+ MIT
@@ -0,0 +1,66 @@
1
+ "use strict";
2
+
3
+ import React from 'react';
4
+ import { View, StyleSheet, Text, Dimensions } from 'react-native';
5
+ import { Camera, useCameraDevice } from 'react-native-vision-camera';
6
+ import { useCardScanner } from "../hooks/useCardScanner.js";
7
+ import { ScannerOverlay } from "./ScannerOverlay.js";
8
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
9
+ const width = Dimensions.get('window').width * 0.95;
10
+ const height = width / 1.586;
11
+ export const CreditCardScanner = ({
12
+ cameraRef,
13
+ isActive,
14
+ onScanSuccess,
15
+ onError,
16
+ style,
17
+ overlayColor = 'rgba(0, 0, 0, 0.6)',
18
+ holeViewConfig = {
19
+ left: 10,
20
+ top: 50,
21
+ width: width,
22
+ height: height
23
+ },
24
+ cameraProps,
25
+ parentViewHeight
26
+ }) => {
27
+ const device = useCameraDevice('back');
28
+ // Custom hook that manages MLKit and the validation pipeline
29
+ const {
30
+ frameOutput
31
+ } = useCardScanner({
32
+ onScanSuccess,
33
+ holeViewConfig,
34
+ parentViewHeight
35
+ });
36
+ if (!device) {
37
+ if (onError) onError(new Error('No back camera device found'));
38
+ return /*#__PURE__*/_jsx(View, {
39
+ style: style,
40
+ children: /*#__PURE__*/_jsx(Text, {
41
+ children: "Camera not available"
42
+ })
43
+ });
44
+ }
45
+ return /*#__PURE__*/_jsxs(View, {
46
+ style: [styles.container, style],
47
+ children: [/*#__PURE__*/_jsx(Camera, {
48
+ ref: cameraRef,
49
+ style: StyleSheet.absoluteFill,
50
+ device: device,
51
+ isActive: isActive,
52
+ outputs: [frameOutput],
53
+ ...cameraProps
54
+ }), /*#__PURE__*/_jsx(ScannerOverlay, {
55
+ color: overlayColor,
56
+ config: holeViewConfig
57
+ })]
58
+ });
59
+ };
60
+ const styles = StyleSheet.create({
61
+ container: {
62
+ overflow: 'hidden',
63
+ position: 'relative'
64
+ }
65
+ });
66
+ //# sourceMappingURL=CreditCardScanner.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["React","View","StyleSheet","Text","Dimensions","Camera","useCameraDevice","useCardScanner","ScannerOverlay","jsx","_jsx","jsxs","_jsxs","width","get","height","CreditCardScanner","cameraRef","isActive","onScanSuccess","onError","style","overlayColor","holeViewConfig","left","top","cameraProps","parentViewHeight","device","frameOutput","Error","children","styles","container","ref","absoluteFill","outputs","color","config","create","overflow","position"],"sourceRoot":"../../../src","sources":["components/CreditCardScanner.tsx"],"mappings":";;AAAA,OAAOA,KAAK,MAAM,OAAO;AACzB,SAASC,IAAI,EAAEC,UAAU,EAAEC,IAAI,EAAEC,UAAU,QAAQ,cAAc;AACjE,SAASC,MAAM,EAAEC,eAAe,QAAQ,4BAA4B;AACpE,SAASC,cAAc,QAAQ,4BAAyB;AACxD,SAASC,cAAc,QAAQ,qBAAkB;AAAC,SAAAC,GAAA,IAAAC,IAAA,EAAAC,IAAA,IAAAC,KAAA;AAElD,MAAMC,KAAK,GAAGT,UAAU,CAACU,GAAG,CAAC,QAAQ,CAAC,CAACD,KAAK,GAAG,IAAI;AACnD,MAAME,MAAM,GAAGF,KAAK,GAAG,KAAK;AAE5B,OAAO,MAAMG,iBAAmD,GAAGA,CAAC;EAClEC,SAAS;EACTC,QAAQ;EACRC,aAAa;EACbC,OAAO;EACPC,KAAK;EACLC,YAAY,GAAG,oBAAoB;EACnCC,cAAc,GAAG;IACfC,IAAI,EAAE,EAAE;IACRC,GAAG,EAAE,EAAE;IACPZ,KAAK,EAAEA,KAAK;IACZE,MAAM,EAAEA;EACV,CAAC;EACDW,WAAW;EACXC;AACF,CAAC,KAAK;EACJ,MAAMC,MAAM,GAAGtB,eAAe,CAAC,MAAM,CAAC;EACtC;EACA,MAAM;IAAEuB;EAAY,CAAC,GAAGtB,cAAc,CAAC;IACrCY,aAAa;IACbI,cAAc;IACdI;EACF,CAAC,CAAC;EAEF,IAAI,CAACC,MAAM,EAAE;IACX,IAAIR,OAAO,EAAEA,OAAO,CAAC,IAAIU,KAAK,CAAC,6BAA6B,CAAC,CAAC;IAC9D,oBACEpB,IAAA,CAACT,IAAI;MAACoB,KAAK,EAAEA,KAAM;MAAAU,QAAA,eACjBrB,IAAA,CAACP,IAAI;QAAA4B,QAAA,EAAC;MAAoB,CAAM;IAAC,CAC7B,CAAC;EAEX;EAEA,oBACEnB,KAAA,CAACX,IAAI;IAACoB,KAAK,EAAE,CAACW,MAAM,CAACC,SAAS,EAAEZ,KAAK,CAAE;IAAAU,QAAA,gBACrCrB,IAAA,CAACL,MAAM;MACL6B,GAAG,EAAEjB,SAAU;MACfI,KAAK,EAAEnB,UAAU,CAACiC,YAAa;MAC/BP,MAAM,EAAEA,MAAO;MACfV,QAAQ,EAAEA,QAAS;MACnBkB,OAAO,EAAE,CAACP,WAAW,CAAE;MAAA,GACnBH;IAAW,CAChB,CAAC,eACFhB,IAAA,CAACF,cAAc;MAAC6B,KAAK,EAAEf,YAAa;MAACgB,MAAM,EAAEf;IAAe,CAAE,CAAC;EAAA,CAC3D,CAAC;AAEX,CAAC;AAED,MAAMS,MAAM,GAAG9B,UAAU,CAACqC,MAAM,CAAC;EAC/BN,SAAS,EAAE;IACTO,QAAQ,EAAE,QAAQ;IAClBC,QAAQ,EAAE;EACZ;AACF,CAAC,CAAC","ignoreList":[]}
@@ -0,0 +1,43 @@
1
+ "use strict";
2
+
3
+ import React from 'react';
4
+ import { StyleSheet, View } from 'react-native';
5
+ import { RNHoleView } from 'react-native-hole-view';
6
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
7
+ export const ScannerOverlay = ({
8
+ color,
9
+ config
10
+ }) => {
11
+ return /*#__PURE__*/_jsxs(View, {
12
+ style: StyleSheet.absoluteFill,
13
+ pointerEvents: "none",
14
+ children: [/*#__PURE__*/_jsx(RNHoleView, {
15
+ style: [StyleSheet.absoluteFill, {
16
+ backgroundColor: color
17
+ }],
18
+ holes: [{
19
+ x: config.left,
20
+ y: config.top,
21
+ width: config.width,
22
+ height: config.height,
23
+ borderRadius: 12
24
+ }]
25
+ }), /*#__PURE__*/_jsx(View, {
26
+ style: [styles.borderOverlay, {
27
+ left: config.left,
28
+ top: config.top,
29
+ width: config.width,
30
+ height: config.height
31
+ }]
32
+ })]
33
+ });
34
+ };
35
+ const styles = StyleSheet.create({
36
+ borderOverlay: {
37
+ // position: 'absolute',
38
+ borderWidth: 2,
39
+ borderColor: 'rgba(255, 255, 255, 0.7)',
40
+ borderRadius: 12
41
+ }
42
+ });
43
+ //# sourceMappingURL=ScannerOverlay.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["React","StyleSheet","View","RNHoleView","jsx","_jsx","jsxs","_jsxs","ScannerOverlay","color","config","style","absoluteFill","pointerEvents","children","backgroundColor","holes","x","left","y","top","width","height","borderRadius","styles","borderOverlay","create","borderWidth","borderColor"],"sourceRoot":"../../../src","sources":["components/ScannerOverlay.tsx"],"mappings":";;AAAA,OAAOA,KAAK,MAAM,OAAO;AACzB,SAASC,UAAU,EAAEC,IAAI,QAAQ,cAAc;AAC/C,SAASC,UAAU,QAAQ,wBAAwB;AAAC,SAAAC,GAAA,IAAAC,IAAA,EAAAC,IAAA,IAAAC,KAAA;AAQpD,OAAO,MAAMC,cAA6C,GAAGA,CAAC;EAC5DC,KAAK;EACLC;AACF,CAAC,KAAK;EACJ,oBACEH,KAAA,CAACL,IAAI;IAACS,KAAK,EAAEV,UAAU,CAACW,YAAa;IAACC,aAAa,EAAC,MAAM;IAAAC,QAAA,gBACxDT,IAAA,CAACF,UAAU;MACTQ,KAAK,EAAE,CAACV,UAAU,CAACW,YAAY,EAAE;QAAEG,eAAe,EAAEN;MAAM,CAAC,CAAE;MAC7DO,KAAK,EAAE,CACL;QACEC,CAAC,EAAEP,MAAM,CAACQ,IAAI;QACdC,CAAC,EAAET,MAAM,CAACU,GAAG;QACbC,KAAK,EAAEX,MAAM,CAACW,KAAK;QACnBC,MAAM,EAAEZ,MAAM,CAACY,MAAM;QACrBC,YAAY,EAAE;MAChB,CAAC;IACD,CACH,CAAC,eAEFlB,IAAA,CAACH,IAAI;MACHS,KAAK,EAAE,CACLa,MAAM,CAACC,aAAa,EACpB;QACEP,IAAI,EAAER,MAAM,CAACQ,IAAI;QACjBE,GAAG,EAAEV,MAAM,CAACU,GAAG;QACfC,KAAK,EAAEX,MAAM,CAACW,KAAK;QACnBC,MAAM,EAAEZ,MAAM,CAACY;MACjB,CAAC;IACD,CACH,CAAC;EAAA,CACE,CAAC;AAEX,CAAC;AAED,MAAME,MAAM,GAAGvB,UAAU,CAACyB,MAAM,CAAC;EAC/BD,aAAa,EAAE;IACb;IACAE,WAAW,EAAE,CAAC;IACdC,WAAW,EAAE,0BAA0B;IACvCL,YAAY,EAAE;EAChB;AACF,CAAC,CAAC","ignoreList":[]}
@@ -0,0 +1,53 @@
1
+ "use strict";
2
+
3
+ import { useTextRecognition } from 'react-native-vision-camera-ocr-plus';
4
+ import { useFrameOutput } from 'react-native-vision-camera';
5
+ import { scheduleOnRN } from 'react-native-worklets';
6
+ import { parseCardData } from "../utils/parsers.js";
7
+ import { isValidLuhn } from "../utils/validators.js";
8
+ import { Dimensions } from 'react-native';
9
+ const {
10
+ width: windowWidth,
11
+ height: windowHeight
12
+ } = Dimensions.get('window');
13
+ export const useCardScanner = ({
14
+ onScanSuccess,
15
+ holeViewConfig,
16
+ parentViewHeight
17
+ }) => {
18
+ const handleSuccess = data => {
19
+ onScanSuccess(data);
20
+ };
21
+ const HEIGHT = parentViewHeight ? parentViewHeight : windowHeight;
22
+ const {
23
+ scanText
24
+ } = useTextRecognition({
25
+ language: 'latin',
26
+ frameSkipThreshold: 10,
27
+ scanRegion: {
28
+ left: `${holeViewConfig.left / windowWidth * 100}%`,
29
+ top: `${holeViewConfig.top / HEIGHT * 100}%`,
30
+ width: `${holeViewConfig.width / windowWidth * 100}%`,
31
+ height: `${holeViewConfig.height / HEIGHT * 100}%`
32
+ }
33
+ });
34
+ const frameOutput = useFrameOutput({
35
+ pixelFormat: 'rgb',
36
+ onFrame: frame => {
37
+ 'worklet';
38
+
39
+ const result = scanText(frame);
40
+ if (result.resultText) {
41
+ const cardData = parseCardData(result.blocks);
42
+ if (cardData.cardNumber && isValidLuhn(cardData.cardNumber)) {
43
+ scheduleOnRN(handleSuccess, cardData);
44
+ }
45
+ }
46
+ frame.dispose();
47
+ }
48
+ });
49
+ return {
50
+ frameOutput
51
+ };
52
+ };
53
+ //# sourceMappingURL=useCardScanner.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["useTextRecognition","useFrameOutput","scheduleOnRN","parseCardData","isValidLuhn","Dimensions","width","windowWidth","height","windowHeight","get","useCardScanner","onScanSuccess","holeViewConfig","parentViewHeight","handleSuccess","data","HEIGHT","scanText","language","frameSkipThreshold","scanRegion","left","top","frameOutput","pixelFormat","onFrame","frame","result","resultText","cardData","blocks","cardNumber","dispose"],"sourceRoot":"../../../src","sources":["hooks/useCardScanner.ts"],"mappings":";;AAAA,SAASA,kBAAkB,QAAQ,qCAAqC;AACxE,SAASC,cAAc,QAAQ,4BAA4B;AAC3D,SAASC,YAAY,QAAQ,uBAAuB;AACpD,SAASC,aAAa,QAAQ,qBAAkB;AAChD,SAASC,WAAW,QAAQ,wBAAqB;AAEjD,SAASC,UAAU,QAAQ,cAAc;AAEzC,MAAM;EAAEC,KAAK,EAAEC,WAAW;EAAEC,MAAM,EAAEC;AAAa,CAAC,GAAGJ,UAAU,CAACK,GAAG,CAAC,QAAQ,CAAC;AAQ7E,OAAO,MAAMC,cAAc,GAAGA,CAAC;EAC7BC,aAAa;EACbC,cAAc;EACdC;AACmB,CAAC,KAAK;EACzB,MAAMC,aAAa,GAAIC,IAAgB,IAAK;IAC1CJ,aAAa,CAACI,IAAI,CAAC;EACrB,CAAC;EACD,MAAMC,MAAM,GAAGH,gBAAgB,GAAGA,gBAAgB,GAAGL,YAAY;EACjE,MAAM;IAAES;EAAS,CAAC,GAAGlB,kBAAkB,CAAC;IACtCmB,QAAQ,EAAE,OAAO;IACjBC,kBAAkB,EAAE,EAAE;IACtBC,UAAU,EAAE;MACVC,IAAI,EAAE,GAAIT,cAAc,CAACS,IAAI,GAAGf,WAAW,GAAI,GAAG,GAAG;MACrDgB,GAAG,EAAE,GAAIV,cAAc,CAACU,GAAG,GAAGN,MAAM,GAAI,GAAG,GAAG;MAC9CX,KAAK,EAAE,GAAIO,cAAc,CAACP,KAAK,GAAGC,WAAW,GAAI,GAAG,GAAG;MACvDC,MAAM,EAAE,GAAIK,cAAc,CAACL,MAAM,GAAGS,MAAM,GAAI,GAAG;IACnD;EACF,CAAC,CAAC;EAEF,MAAMO,WAAW,GAAGvB,cAAc,CAAC;IACjCwB,WAAW,EAAE,KAAK;IAClBC,OAAO,EAAGC,KAAK,IAAK;MAClB,SAAS;;MAET,MAAMC,MAAM,GAAGV,QAAQ,CAACS,KAAK,CAAC;MAC9B,IAAIC,MAAM,CAACC,UAAU,EAAE;QACrB,MAAMC,QAAQ,GAAG3B,aAAa,CAACyB,MAAM,CAACG,MAAM,CAAC;QAE7C,IAAID,QAAQ,CAACE,UAAU,IAAI5B,WAAW,CAAC0B,QAAQ,CAACE,UAAU,CAAC,EAAE;UAC3D9B,YAAY,CAACa,aAAa,EAAEe,QAAsB,CAAC;QACrD;MACF;MAEAH,KAAK,CAACM,OAAO,CAAC,CAAC;IACjB;EACF,CAAC,CAAC;EAEF,OAAO;IAAET;EAAY,CAAC;AACxB,CAAC","ignoreList":[]}
@@ -0,0 +1,5 @@
1
+ "use strict";
2
+
3
+ export * from "./components/CreditCardScanner.js";
4
+ export * from "./types/index.js";
5
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":[],"sourceRoot":"../../src","sources":["index.tsx"],"mappings":";;AAAA,cAAc,mCAAgC;AAC9C,cAAc,kBAAS","ignoreList":[]}
@@ -0,0 +1 @@
1
+ {"type":"module"}
@@ -0,0 +1,4 @@
1
+ "use strict";
2
+
3
+ export {};
4
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":[],"sourceRoot":"../../../src","sources":["types/index.ts"],"mappings":"","ignoreList":[]}
@@ -0,0 +1,75 @@
1
+ "use strict";
2
+
3
+ import { getCardIssuer } from "./validators.js";
4
+ const CARD_KEYWORDS_REGEX = /\b(VISA|MASTERCARD|DISCOVER|AMERICAN|EXPRESS|AMEX|RUPAY|MAESTRO|CIRRUS|DEBIT|CREDIT|PREPAID|PLATINUM|GOLD|CLASSIC|SIGNATURE|INFINITE|WORLD|ELITE|BUSINESS|CORPORATE|BANK|CARD|SERVICES|FINANCIAL|TRUST|UNION|FEDERAL|NATIONAL|INTERNATIONAL|GLOBAL|PLUS|REWARDS|CASHBACK|VALID|THRU|FROM|UNTIL|EXPIRES|EXP|DATE|GOOD|MEMBER|SINCE|CVV|CVC|CID|AUTHORIZED|PROPERTY|RETURN|CUSTOMER|SERVICE|ACCOUNT|SAVING)\b/i;
5
+ function isValidNameLine(text) {
6
+ 'worklet';
7
+
8
+ // 1. Clean up whitespace
9
+ const cleanedText = text.trim();
10
+
11
+ // 2. Ignore if it contains any numbers (names don't have digits)
12
+ if (/\d/.test(cleanedText)) return false;
13
+
14
+ // 3. Ignore if it contains common card boilerplate keywords
15
+ const hasKeywords = CARD_KEYWORDS_REGEX.test(cleanedText);
16
+ if (hasKeywords) return false;
17
+
18
+ // 4. Ignore if it's too short to be a full name (e.g., single letters or noise)
19
+ if (cleanedText.length < 4 || cleanedText.length > 30) return false;
20
+ return true;
21
+ }
22
+ export const parseCardData = blocks => {
23
+ 'worklet';
24
+
25
+ let cardNumber;
26
+ let expiryDate = null;
27
+ let cardholderName;
28
+
29
+ // Potential name block lines
30
+ const potentialNames = [];
31
+ for (const block of blocks) {
32
+ const text = block.blockText.trim();
33
+ // Normalize string to help with matching
34
+ const normalizedText = text.replace(/[\r\n]+/g, ' ');
35
+
36
+ // 1. Extract Card Number
37
+ // Looks for 13 to 19 digits, possibly separated by spaces or dashes
38
+ if (!cardNumber) {
39
+ const cardNumMatch = normalizedText.match(/(?:\d[ -]*?){13,19}/);
40
+ if (cardNumMatch) {
41
+ const potentialNumber = cardNumMatch[0].replace(/\D/g, '');
42
+ if (potentialNumber.length >= 13 && potentialNumber.length <= 19) {
43
+ cardNumber = potentialNumber;
44
+ continue; // Skip further parsing on this block if it's the card number
45
+ }
46
+ }
47
+ }
48
+
49
+ // 2. Extract Expiry Date (MM/YY or MM/YYYY)
50
+ if (!expiryDate) {
51
+ const expiryMatch = normalizedText.match(/\b(0[1-9]|1[0-2])\s*[\/\-]\s*(\d{2}|\d{4})\b/);
52
+ if (expiryMatch) {
53
+ expiryDate = `${expiryMatch[1]}/${expiryMatch[2]}`;
54
+ continue;
55
+ }
56
+ }
57
+
58
+ // 3. Name heuristics
59
+ if (isValidNameLine(text)) {
60
+ potentialNames.push(text);
61
+ }
62
+ }
63
+
64
+ // Very basic heuristic for name: take the longest block that matches our criteria
65
+ if (potentialNames.length > 0) {
66
+ cardholderName = potentialNames.sort((a, b) => b.length - a.length)[0];
67
+ }
68
+ return {
69
+ cardNumber: cardNumber || '',
70
+ expiryDate,
71
+ cardholderName,
72
+ issuer: cardNumber ? getCardIssuer(cardNumber) : 'Unknown'
73
+ };
74
+ };
75
+ //# sourceMappingURL=parsers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["getCardIssuer","CARD_KEYWORDS_REGEX","isValidNameLine","text","cleanedText","trim","test","hasKeywords","length","parseCardData","blocks","cardNumber","expiryDate","cardholderName","potentialNames","block","blockText","normalizedText","replace","cardNumMatch","match","potentialNumber","expiryMatch","push","sort","a","b","issuer"],"sourceRoot":"../../../src","sources":["utils/parsers.ts"],"mappings":";;AACA,SAASA,aAAa,QAAQ,iBAAc;AAM5C,MAAMC,mBAAmB,GACvB,8YAA8Y;AAEhZ,SAASC,eAAeA,CAACC,IAAY,EAAE;EACrC,SAAS;;EACT;EACA,MAAMC,WAAW,GAAGD,IAAI,CAACE,IAAI,CAAC,CAAC;;EAE/B;EACA,IAAI,IAAI,CAACC,IAAI,CAACF,WAAW,CAAC,EAAE,OAAO,KAAK;;EAExC;EACA,MAAMG,WAAW,GAAGN,mBAAmB,CAACK,IAAI,CAACF,WAAW,CAAC;EACzD,IAAIG,WAAW,EAAE,OAAO,KAAK;;EAE7B;EACA,IAAIH,WAAW,CAACI,MAAM,GAAG,CAAC,IAAIJ,WAAW,CAACI,MAAM,GAAG,EAAE,EAAE,OAAO,KAAK;EAEnE,OAAO,IAAI;AACb;AAEA,OAAO,MAAMC,aAAa,GAAIC,MAAkB,IAA0B;EACxE,SAAS;;EACT,IAAIC,UAA8B;EAClC,IAAIC,UAAyB,GAAG,IAAI;EACpC,IAAIC,cAAkC;;EAEtC;EACA,MAAMC,cAAwB,GAAG,EAAE;EAEnC,KAAK,MAAMC,KAAK,IAAIL,MAAM,EAAE;IAC1B,MAAMP,IAAI,GAAGY,KAAK,CAACC,SAAS,CAACX,IAAI,CAAC,CAAC;IACnC;IACA,MAAMY,cAAc,GAAGd,IAAI,CAACe,OAAO,CAAC,UAAU,EAAE,GAAG,CAAC;;IAEpD;IACA;IACA,IAAI,CAACP,UAAU,EAAE;MACf,MAAMQ,YAAY,GAAGF,cAAc,CAACG,KAAK,CAAC,qBAAqB,CAAC;MAChE,IAAID,YAAY,EAAE;QAChB,MAAME,eAAe,GAAGF,YAAY,CAAC,CAAC,CAAC,CAACD,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC;QAC1D,IAAIG,eAAe,CAACb,MAAM,IAAI,EAAE,IAAIa,eAAe,CAACb,MAAM,IAAI,EAAE,EAAE;UAChEG,UAAU,GAAGU,eAAe;UAC5B,SAAS,CAAC;QACZ;MACF;IACF;;IAEA;IACA,IAAI,CAACT,UAAU,EAAE;MACf,MAAMU,WAAW,GAAGL,cAAc,CAACG,KAAK,CACtC,8CACF,CAAC;MACD,IAAIE,WAAW,EAAE;QACfV,UAAU,GAAG,GAAGU,WAAW,CAAC,CAAC,CAAC,IAAIA,WAAW,CAAC,CAAC,CAAC,EAAE;QAClD;MACF;IACF;;IAEA;IACA,IAAIpB,eAAe,CAACC,IAAI,CAAC,EAAE;MACzBW,cAAc,CAACS,IAAI,CAACpB,IAAI,CAAC;IAC3B;EACF;;EAEA;EACA,IAAIW,cAAc,CAACN,MAAM,GAAG,CAAC,EAAE;IAC7BK,cAAc,GAAGC,cAAc,CAACU,IAAI,CAAC,CAACC,CAAC,EAAEC,CAAC,KAAKA,CAAC,CAAClB,MAAM,GAAGiB,CAAC,CAACjB,MAAM,CAAC,CAAC,CAAC,CAAC;EACxE;EACA,OAAO;IACLG,UAAU,EAAEA,UAAU,IAAI,EAAE;IAC5BC,UAAU;IACVC,cAAc;IACdc,MAAM,EAAEhB,UAAU,GAAGX,aAAa,CAACW,UAAU,CAAC,GAAG;EACnD,CAAC;AACH,CAAC","ignoreList":[]}
@@ -0,0 +1,65 @@
1
+ "use strict";
2
+
3
+ export const isValidLuhn = cardNumber => {
4
+ 'worklet';
5
+
6
+ // Remove all non-digit characters
7
+ const cleanNumber = cardNumber.replace(/\D/g, '');
8
+ if (cleanNumber.length === 0) return false;
9
+ let sum = 0;
10
+ let isEven = false;
11
+
12
+ // Loop from right to left
13
+ for (let i = cleanNumber.length - 1; i >= 0; i--) {
14
+ let digit = parseInt(cleanNumber.charAt(i), 10);
15
+ if (isEven) {
16
+ digit *= 2;
17
+ if (digit > 9) {
18
+ digit -= 9;
19
+ }
20
+ }
21
+ sum += digit;
22
+ isEven = !isEven;
23
+ }
24
+ return sum % 10 === 0;
25
+ };
26
+ export const getCardIssuer = cardNumber => {
27
+ 'worklet';
28
+
29
+ const cleanNumber = cardNumber.replace(/\D/g, '');
30
+
31
+ // 1. VISA – must come before UnionPay (some 4* UnionPay co-brands exist)
32
+ if (/^4/.test(cleanNumber)) return 'Visa';
33
+
34
+ // 2. MASTERCARD – 51-55 OR 2221-2720
35
+ if (/^5[1-5]/.test(cleanNumber) || /^(222[1-9]|22[3-9]\d|2[3-6]\d{2}|27[01]\d|2720)/.test(cleanNumber)) return 'Mastercard';
36
+
37
+ // 3. AMEX
38
+ if (/^3[47]/.test(cleanNumber)) return 'Amex';
39
+
40
+ // 4. DINERS CLUB – before JCB (both start with 3)
41
+ if (/^(30[0-5]|3095|36|3[89])/.test(cleanNumber)) return 'Diners Club';
42
+
43
+ // 5. JCB – 3528-3589
44
+ if (/^35(2[89]|[3-8]\d)/.test(cleanNumber)) return 'JCB';
45
+
46
+ // 6. MIR (Russia)
47
+ if (/^220[0-4]/.test(cleanNumber)) return 'Mir';
48
+
49
+ // 7. RUPAY – check before Discover (shares 60/65 prefixes)
50
+ if (/^(508|353|356|652[12]|6[0-9]{3}(?=.*81|.*82)|81[0-9]|82[0-9])/.test(cleanNumber) || /^(508[0-9]|6521|6522)/.test(cleanNumber) || /^(60[2-9]|81|82)/.test(cleanNumber)) return 'RuPay';
51
+
52
+ // 8. DISCOVER – 6011, 644-649, 65, 622126-622925
53
+ if (/^6011/.test(cleanNumber) || /^64[4-9]/.test(cleanNumber) || /^65/.test(cleanNumber) || /^622(1(2[6-9]|[3-9]\d)|[2-8]\d{2}|9([01]\d|2[0-5]))/.test(cleanNumber)) return 'Discover';
54
+
55
+ // 9. CHINA UNIONPAY – broad 62 catch (after Discover's 622* check)
56
+ if (/^62/.test(cleanNumber)) return 'UnionPay';
57
+
58
+ // 10. MAESTRO
59
+ if (/^(5018|5020|5038|5893|6304|6759|676[1-3])/.test(cleanNumber)) return 'Maestro';
60
+
61
+ // 11. INSTAPAYMENT
62
+ if (/^63[7-9]/.test(cleanNumber)) return 'InstaPayment';
63
+ return 'Unknown';
64
+ };
65
+ //# sourceMappingURL=validators.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["isValidLuhn","cardNumber","cleanNumber","replace","length","sum","isEven","i","digit","parseInt","charAt","getCardIssuer","test"],"sourceRoot":"../../../src","sources":["utils/validators.ts"],"mappings":";;AAEA,OAAO,MAAMA,WAAW,GAAIC,UAAkB,IAAc;EAC1D,SAAS;;EACT;EACA,MAAMC,WAAW,GAAGD,UAAU,CAACE,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC;EACjD,IAAID,WAAW,CAACE,MAAM,KAAK,CAAC,EAAE,OAAO,KAAK;EAE1C,IAAIC,GAAG,GAAG,CAAC;EACX,IAAIC,MAAM,GAAG,KAAK;;EAElB;EACA,KAAK,IAAIC,CAAC,GAAGL,WAAW,CAACE,MAAM,GAAG,CAAC,EAAEG,CAAC,IAAI,CAAC,EAAEA,CAAC,EAAE,EAAE;IAChD,IAAIC,KAAK,GAAGC,QAAQ,CAACP,WAAW,CAACQ,MAAM,CAACH,CAAC,CAAC,EAAE,EAAE,CAAC;IAE/C,IAAID,MAAM,EAAE;MACVE,KAAK,IAAI,CAAC;MACV,IAAIA,KAAK,GAAG,CAAC,EAAE;QACbA,KAAK,IAAI,CAAC;MACZ;IACF;IAEAH,GAAG,IAAIG,KAAK;IACZF,MAAM,GAAG,CAACA,MAAM;EAClB;EAEA,OAAOD,GAAG,GAAG,EAAE,KAAK,CAAC;AACvB,CAAC;AAED,OAAO,MAAMM,aAAa,GAAIV,UAAkB,IAA2B;EACzE,SAAS;;EACT,MAAMC,WAAW,GAAGD,UAAU,CAACE,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC;;EAEjD;EACA,IAAI,IAAI,CAACS,IAAI,CAACV,WAAW,CAAC,EAAE,OAAO,MAAM;;EAEzC;EACA,IACE,SAAS,CAACU,IAAI,CAACV,WAAW,CAAC,IAC3B,iDAAiD,CAACU,IAAI,CAACV,WAAW,CAAC,EAEnE,OAAO,YAAY;;EAErB;EACA,IAAI,QAAQ,CAACU,IAAI,CAACV,WAAW,CAAC,EAAE,OAAO,MAAM;;EAE7C;EACA,IAAI,0BAA0B,CAACU,IAAI,CAACV,WAAW,CAAC,EAAE,OAAO,aAAa;;EAEtE;EACA,IAAI,oBAAoB,CAACU,IAAI,CAACV,WAAW,CAAC,EAAE,OAAO,KAAK;;EAExD;EACA,IAAI,WAAW,CAACU,IAAI,CAACV,WAAW,CAAC,EAAE,OAAO,KAAK;;EAE/C;EACA,IACE,+DAA+D,CAACU,IAAI,CAClEV,WACF,CAAC,IACD,uBAAuB,CAACU,IAAI,CAACV,WAAW,CAAC,IACzC,kBAAkB,CAACU,IAAI,CAACV,WAAW,CAAC,EAEpC,OAAO,OAAO;;EAEhB;EACA,IACE,OAAO,CAACU,IAAI,CAACV,WAAW,CAAC,IACzB,UAAU,CAACU,IAAI,CAACV,WAAW,CAAC,IAC5B,KAAK,CAACU,IAAI,CAACV,WAAW,CAAC,IACvB,qDAAqD,CAACU,IAAI,CAACV,WAAW,CAAC,EAEvE,OAAO,UAAU;;EAEnB;EACA,IAAI,KAAK,CAACU,IAAI,CAACV,WAAW,CAAC,EAAE,OAAO,UAAU;;EAE9C;EACA,IAAI,2CAA2C,CAACU,IAAI,CAACV,WAAW,CAAC,EAC/D,OAAO,SAAS;;EAElB;EACA,IAAI,UAAU,CAACU,IAAI,CAACV,WAAW,CAAC,EAAE,OAAO,cAAc;EAEvD,OAAO,SAAS;AAClB,CAAC","ignoreList":[]}
@@ -0,0 +1 @@
1
+ {"type":"module"}
@@ -0,0 +1,4 @@
1
+ import React from 'react';
2
+ import { type CreditCardScannerProps } from '../types';
3
+ export declare const CreditCardScanner: React.FC<CreditCardScannerProps>;
4
+ //# sourceMappingURL=CreditCardScanner.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CreditCardScanner.d.ts","sourceRoot":"","sources":["../../../../src/components/CreditCardScanner.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAK1B,OAAO,EAAE,KAAK,sBAAsB,EAAE,MAAM,UAAU,CAAC;AAIvD,eAAO,MAAM,iBAAiB,EAAE,KAAK,CAAC,EAAE,CAAC,sBAAsB,CA8C9D,CAAC"}
@@ -0,0 +1,9 @@
1
+ import React from 'react';
2
+ import { type HoleViewConfig } from '../types';
3
+ interface ScannerOverlayProps {
4
+ color: string;
5
+ config: HoleViewConfig;
6
+ }
7
+ export declare const ScannerOverlay: React.FC<ScannerOverlayProps>;
8
+ export {};
9
+ //# sourceMappingURL=ScannerOverlay.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ScannerOverlay.d.ts","sourceRoot":"","sources":["../../../../src/components/ScannerOverlay.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAG1B,OAAO,EAAE,KAAK,cAAc,EAAE,MAAM,UAAU,CAAC;AAE/C,UAAU,mBAAmB;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,cAAc,CAAC;CACxB;AAED,eAAO,MAAM,cAAc,EAAE,KAAK,CAAC,EAAE,CAAC,mBAAmB,CAgCxD,CAAC"}
@@ -0,0 +1,11 @@
1
+ import { type CardResult, type HoleViewConfig } from '../types';
2
+ interface UseCardScannerProps {
3
+ onScanSuccess: (result: CardResult) => void;
4
+ holeViewConfig: HoleViewConfig;
5
+ parentViewHeight?: number;
6
+ }
7
+ export declare const useCardScanner: ({ onScanSuccess, holeViewConfig, parentViewHeight, }: UseCardScannerProps) => {
8
+ frameOutput: import("react-native-vision-camera").CameraFrameOutput;
9
+ };
10
+ export {};
11
+ //# sourceMappingURL=useCardScanner.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useCardScanner.d.ts","sourceRoot":"","sources":["../../../../src/hooks/useCardScanner.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,KAAK,UAAU,EAAE,KAAK,cAAc,EAAE,MAAM,UAAU,CAAC;AAKhE,UAAU,mBAAmB;IAC3B,aAAa,EAAE,CAAC,MAAM,EAAE,UAAU,KAAK,IAAI,CAAC;IAC5C,cAAc,EAAE,cAAc,CAAC;IAC/B,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,eAAO,MAAM,cAAc,GAAI,sDAI5B,mBAAmB;;CAmCrB,CAAC"}
@@ -0,0 +1,3 @@
1
+ export * from './components/CreditCardScanner';
2
+ export * from './types';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/index.tsx"],"names":[],"mappings":"AAAA,cAAc,gCAAgC,CAAC;AAC/C,cAAc,SAAS,CAAC"}
@@ -0,0 +1,35 @@
1
+ import { type ViewStyle, type StyleProp } from 'react-native';
2
+ import { type CameraProps, type CameraRef } from 'react-native-vision-camera';
3
+ export interface CardResult {
4
+ cardNumber: string;
5
+ expiryDate: string | null;
6
+ cardholderName: string | null;
7
+ issuer: 'Visa' | 'Mastercard' | 'Amex' | 'RuPay' | 'Discover' | 'Diners Club' | 'JCB' | 'Mir' | 'UnionPay' | 'Maestro' | 'InstaPayment' | 'Unknown';
8
+ }
9
+ export interface HoleViewConfig {
10
+ left: number;
11
+ top: number;
12
+ width: number;
13
+ height: number;
14
+ }
15
+ export interface CreditCardScannerProps {
16
+ /** Ref to controll camera programatically */
17
+ cameraRef?: React.RefObject<CameraRef | null> | undefined;
18
+ /** Enables or disables the camera and frame processing */
19
+ isActive: boolean;
20
+ /** Callback fired when a valid card is detected (passes Luhn check) */
21
+ onScanSuccess: (result: CardResult) => void;
22
+ /** Callback for camera initialization or permission errors */
23
+ onError?: (error: Error) => void;
24
+ /** Container style to allow full-screen or partial-screen rendering */
25
+ style?: StyleProp<ViewStyle>;
26
+ /** Optional: Customize the HoleView overlay mask */
27
+ overlayColor?: string;
28
+ /** Optional: Customize the cutout coordinates */
29
+ holeViewConfig?: HoleViewConfig;
30
+ /** Optional: Pass down native camera props (e.g., enable torch) */
31
+ cameraProps?: Partial<CameraProps>;
32
+ /** Optional: Pass parent height to align hole view correctly with scan region */
33
+ parentViewHeight?: number;
34
+ }
35
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/types/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,SAAS,EAAE,KAAK,SAAS,EAAE,MAAM,cAAc,CAAC;AAC9D,OAAO,EAAE,KAAK,WAAW,EAAE,KAAK,SAAS,EAAE,MAAM,4BAA4B,CAAC;AAE9E,MAAM,WAAW,UAAU;IACzB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,MAAM,EACF,MAAM,GACN,YAAY,GACZ,MAAM,GACN,OAAO,GACP,UAAU,GACV,aAAa,GACb,KAAK,GACL,KAAK,GACL,UAAU,GACV,SAAS,GACT,cAAc,GACd,SAAS,CAAC;CACf;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,sBAAsB;IACrC,6CAA6C;IAC7C,SAAS,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC,SAAS,GAAG,IAAI,CAAC,GAAG,SAAS,CAAC;IAC1D,0DAA0D;IAC1D,QAAQ,EAAE,OAAO,CAAC;IAElB,uEAAuE;IACvE,aAAa,EAAE,CAAC,MAAM,EAAE,UAAU,KAAK,IAAI,CAAC;IAE5C,8DAA8D;IAC9D,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;IAEjC,uEAAuE;IACvE,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAE7B,oDAAoD;IACpD,YAAY,CAAC,EAAE,MAAM,CAAC;IAEtB,iDAAiD;IACjD,cAAc,CAAC,EAAE,cAAc,CAAC;IAEhC,mEAAmE;IACnE,WAAW,CAAC,EAAE,OAAO,CAAC,WAAW,CAAC,CAAC;IAEnC,iFAAiF;IACjF,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B"}
@@ -0,0 +1,6 @@
1
+ import { type CardResult } from '../types';
2
+ export interface OCRBlock {
3
+ blockText: string;
4
+ }
5
+ export declare const parseCardData: (blocks: OCRBlock[]) => Partial<CardResult>;
6
+ //# sourceMappingURL=parsers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parsers.d.ts","sourceRoot":"","sources":["../../../../src/utils/parsers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,UAAU,EAAE,MAAM,UAAU,CAAC;AAG3C,MAAM,WAAW,QAAQ;IACvB,SAAS,EAAE,MAAM,CAAC;CACnB;AAuBD,eAAO,MAAM,aAAa,GAAI,QAAQ,QAAQ,EAAE,KAAG,OAAO,CAAC,UAAU,CAsDpE,CAAC"}
@@ -0,0 +1,4 @@
1
+ import { type CardResult } from '../types';
2
+ export declare const isValidLuhn: (cardNumber: string) => boolean;
3
+ export declare const getCardIssuer: (cardNumber: string) => CardResult["issuer"];
4
+ //# sourceMappingURL=validators.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validators.d.ts","sourceRoot":"","sources":["../../../../src/utils/validators.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,UAAU,EAAE,MAAM,UAAU,CAAC;AAE3C,eAAO,MAAM,WAAW,GAAI,YAAY,MAAM,KAAG,OAyBhD,CAAC;AAEF,eAAO,MAAM,aAAa,GAAI,YAAY,MAAM,KAAG,UAAU,CAAC,QAAQ,CAwDrE,CAAC"}
package/package.json ADDED
@@ -0,0 +1,137 @@
1
+ {
2
+ "name": "react-native-credit-card-scanner",
3
+ "version": "1.0.0",
4
+ "description": "A high-performance credit card scanning library for React Native",
5
+ "main": "./lib/module/index.js",
6
+ "types": "./lib/typescript/src/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "source": "./src/index.tsx",
10
+ "types": "./lib/typescript/src/index.d.ts",
11
+ "default": "./lib/module/index.js"
12
+ },
13
+ "./package.json": "./package.json"
14
+ },
15
+ "files": [
16
+ "src",
17
+ "lib",
18
+ "android",
19
+ "ios",
20
+ "cpp",
21
+ "*.podspec",
22
+ "react-native.config.js",
23
+ "!ios/build",
24
+ "!android/build",
25
+ "!android/gradle",
26
+ "!android/gradlew",
27
+ "!android/gradlew.bat",
28
+ "!android/local.properties",
29
+ "!**/__tests__",
30
+ "!**/__fixtures__",
31
+ "!**/__mocks__",
32
+ "!**/.*"
33
+ ],
34
+ "scripts": {
35
+ "example": "yarn workspace react-native-credit-card-scanner-example",
36
+ "clean": "del-cli android/build example/android/build example/android/app/build example/ios/build lib",
37
+ "prepare": "bob build",
38
+ "typecheck": "tsc",
39
+ "lint": "eslint \"**/*.{js,ts,tsx}\"",
40
+ "test": "jest"
41
+ },
42
+ "keywords": [
43
+ "react-native",
44
+ "ios",
45
+ "android"
46
+ ],
47
+ "repository": {
48
+ "type": "git",
49
+ "url": "git+https://github.com/rvibit/react-native-credit-card-scanner"
50
+ },
51
+ "author": "ravi singh <singh.ravi488@gmail.com> (https://github.com/rvibit)",
52
+ "license": "MIT",
53
+ "bugs": {
54
+ "url": "https://github.com/rvibit/react-native-credit-card-scanner/issues"
55
+ },
56
+ "homepage": "https://github.com/rvibit/react-native-credit-card-scanner#readme",
57
+ "publishConfig": {
58
+ "registry": "https://registry.npmjs.org/"
59
+ },
60
+ "devDependencies": {
61
+ "@eslint/compat": "^2.0.3",
62
+ "@eslint/eslintrc": "^3.3.5",
63
+ "@eslint/js": "^9",
64
+ "@react-native/babel-preset": "0.85.0",
65
+ "@react-native/eslint-config": "0.85.0",
66
+ "@types/jest": "^30.0.0",
67
+ "@types/react": "^19.2.0",
68
+ "del-cli": "^7.0.0",
69
+ "eslint": "^9.39.4",
70
+ "eslint-config-prettier": "^10.1.8",
71
+ "eslint-plugin-ft-flow": "^3.0.11",
72
+ "eslint-plugin-prettier": "^5.5.5",
73
+ "jest": "^30.4.2",
74
+ "prettier": "^3.8.1",
75
+ "react": "19.2.3",
76
+ "react-native": "0.85.0",
77
+ "react-native-builder-bob": "^0.41.0",
78
+ "react-native-hole-view": "^5.0.0",
79
+ "react-native-monorepo-config": "^0.3.3",
80
+ "react-native-nitro-image": "^0.15.1",
81
+ "react-native-nitro-modules": "^0.35.9",
82
+ "react-native-vision-camera": ">=5.0.0",
83
+ "react-native-vision-camera-ocr-plus": "^2.0.0",
84
+ "react-native-vision-camera-worklets": "^5.0.11",
85
+ "react-native-worklets": "*",
86
+ "turbo": "^2.8.21",
87
+ "typescript": "^6.0.2"
88
+ },
89
+ "peerDependencies": {
90
+ "react": "*",
91
+ "react-native": "*",
92
+ "react-native-hole-view": "^5.0.0",
93
+ "react-native-nitro-image": "^0.15.1",
94
+ "react-native-nitro-modules": "^0.35.9",
95
+ "react-native-vision-camera": ">=5.0.0",
96
+ "react-native-vision-camera-ocr-plus": "*",
97
+ "react-native-vision-camera-worklets": "^5.0.11",
98
+ "react-native-worklets": "*"
99
+ },
100
+ "workspaces": [
101
+ "example"
102
+ ],
103
+ "packageManager": "yarn@4.11.0",
104
+ "react-native-builder-bob": {
105
+ "source": "src",
106
+ "output": "lib",
107
+ "targets": [
108
+ [
109
+ "module",
110
+ {
111
+ "esm": true
112
+ }
113
+ ],
114
+ [
115
+ "typescript",
116
+ {
117
+ "project": "tsconfig.build.json"
118
+ }
119
+ ]
120
+ ]
121
+ },
122
+ "prettier": {
123
+ "quoteProps": "consistent",
124
+ "singleQuote": true,
125
+ "tabWidth": 2,
126
+ "trailingComma": "es5",
127
+ "useTabs": false
128
+ },
129
+ "create-react-native-library": {
130
+ "type": "library",
131
+ "languages": "js",
132
+ "tools": [
133
+ "eslint"
134
+ ],
135
+ "version": "0.62.0"
136
+ }
137
+ }
@@ -0,0 +1,63 @@
1
+ import React from 'react';
2
+ import { View, StyleSheet, Text, Dimensions } from 'react-native';
3
+ import { Camera, useCameraDevice } from 'react-native-vision-camera';
4
+ import { useCardScanner } from '../hooks/useCardScanner';
5
+ import { ScannerOverlay } from './ScannerOverlay';
6
+ import { type CreditCardScannerProps } from '../types';
7
+ const width = Dimensions.get('window').width * 0.95;
8
+ const height = width / 1.586;
9
+
10
+ export const CreditCardScanner: React.FC<CreditCardScannerProps> = ({
11
+ cameraRef,
12
+ isActive,
13
+ onScanSuccess,
14
+ onError,
15
+ style,
16
+ overlayColor = 'rgba(0, 0, 0, 0.6)',
17
+ holeViewConfig = {
18
+ left: 10,
19
+ top: 50,
20
+ width: width,
21
+ height: height,
22
+ },
23
+ cameraProps,
24
+ parentViewHeight,
25
+ }) => {
26
+ const device = useCameraDevice('back');
27
+ // Custom hook that manages MLKit and the validation pipeline
28
+ const { frameOutput } = useCardScanner({
29
+ onScanSuccess,
30
+ holeViewConfig,
31
+ parentViewHeight,
32
+ });
33
+
34
+ if (!device) {
35
+ if (onError) onError(new Error('No back camera device found'));
36
+ return (
37
+ <View style={style}>
38
+ <Text>Camera not available</Text>
39
+ </View>
40
+ );
41
+ }
42
+
43
+ return (
44
+ <View style={[styles.container, style]}>
45
+ <Camera
46
+ ref={cameraRef}
47
+ style={StyleSheet.absoluteFill}
48
+ device={device}
49
+ isActive={isActive}
50
+ outputs={[frameOutput]}
51
+ {...cameraProps}
52
+ />
53
+ <ScannerOverlay color={overlayColor} config={holeViewConfig} />
54
+ </View>
55
+ );
56
+ };
57
+
58
+ const styles = StyleSheet.create({
59
+ container: {
60
+ overflow: 'hidden',
61
+ position: 'relative',
62
+ },
63
+ });
@@ -0,0 +1,52 @@
1
+ import React from 'react';
2
+ import { StyleSheet, View } from 'react-native';
3
+ import { RNHoleView } from 'react-native-hole-view';
4
+ import { type HoleViewConfig } from '../types';
5
+
6
+ interface ScannerOverlayProps {
7
+ color: string;
8
+ config: HoleViewConfig;
9
+ }
10
+
11
+ export const ScannerOverlay: React.FC<ScannerOverlayProps> = ({
12
+ color,
13
+ config,
14
+ }) => {
15
+ return (
16
+ <View style={StyleSheet.absoluteFill} pointerEvents="none">
17
+ <RNHoleView
18
+ style={[StyleSheet.absoluteFill, { backgroundColor: color }]}
19
+ holes={[
20
+ {
21
+ x: config.left,
22
+ y: config.top,
23
+ width: config.width,
24
+ height: config.height,
25
+ borderRadius: 12,
26
+ },
27
+ ]}
28
+ />
29
+ {/* Draw a subtle border around the hole cutout */}
30
+ <View
31
+ style={[
32
+ styles.borderOverlay,
33
+ {
34
+ left: config.left,
35
+ top: config.top,
36
+ width: config.width,
37
+ height: config.height,
38
+ },
39
+ ]}
40
+ />
41
+ </View>
42
+ );
43
+ };
44
+
45
+ const styles = StyleSheet.create({
46
+ borderOverlay: {
47
+ // position: 'absolute',
48
+ borderWidth: 2,
49
+ borderColor: 'rgba(255, 255, 255, 0.7)',
50
+ borderRadius: 12,
51
+ },
52
+ });
@@ -0,0 +1,56 @@
1
+ import { useTextRecognition } from 'react-native-vision-camera-ocr-plus';
2
+ import { useFrameOutput } from 'react-native-vision-camera';
3
+ import { scheduleOnRN } from 'react-native-worklets';
4
+ import { parseCardData } from '../utils/parsers';
5
+ import { isValidLuhn } from '../utils/validators';
6
+ import { type CardResult, type HoleViewConfig } from '../types';
7
+ import { Dimensions } from 'react-native';
8
+
9
+ const { width: windowWidth, height: windowHeight } = Dimensions.get('window');
10
+
11
+ interface UseCardScannerProps {
12
+ onScanSuccess: (result: CardResult) => void;
13
+ holeViewConfig: HoleViewConfig;
14
+ parentViewHeight?: number;
15
+ }
16
+
17
+ export const useCardScanner = ({
18
+ onScanSuccess,
19
+ holeViewConfig,
20
+ parentViewHeight,
21
+ }: UseCardScannerProps) => {
22
+ const handleSuccess = (data: CardResult) => {
23
+ onScanSuccess(data);
24
+ };
25
+ const HEIGHT = parentViewHeight ? parentViewHeight : windowHeight;
26
+ const { scanText } = useTextRecognition({
27
+ language: 'latin',
28
+ frameSkipThreshold: 10,
29
+ scanRegion: {
30
+ left: `${(holeViewConfig.left / windowWidth) * 100}%`,
31
+ top: `${(holeViewConfig.top / HEIGHT) * 100}%`,
32
+ width: `${(holeViewConfig.width / windowWidth) * 100}%`,
33
+ height: `${(holeViewConfig.height / HEIGHT) * 100}%`,
34
+ },
35
+ });
36
+
37
+ const frameOutput = useFrameOutput({
38
+ pixelFormat: 'rgb',
39
+ onFrame: (frame) => {
40
+ 'worklet';
41
+
42
+ const result = scanText(frame);
43
+ if (result.resultText) {
44
+ const cardData = parseCardData(result.blocks);
45
+
46
+ if (cardData.cardNumber && isValidLuhn(cardData.cardNumber)) {
47
+ scheduleOnRN(handleSuccess, cardData as CardResult);
48
+ }
49
+ }
50
+
51
+ frame.dispose();
52
+ },
53
+ });
54
+
55
+ return { frameOutput };
56
+ };
package/src/index.tsx ADDED
@@ -0,0 +1,2 @@
1
+ export * from './components/CreditCardScanner';
2
+ export * from './types';
@@ -0,0 +1,56 @@
1
+ import { type ViewStyle, type StyleProp } from 'react-native';
2
+ import { type CameraProps, type CameraRef } from 'react-native-vision-camera';
3
+
4
+ export interface CardResult {
5
+ cardNumber: string;
6
+ expiryDate: string | null; // Format: MM/YY
7
+ cardholderName: string | null;
8
+ issuer:
9
+ | 'Visa'
10
+ | 'Mastercard'
11
+ | 'Amex'
12
+ | 'RuPay'
13
+ | 'Discover'
14
+ | 'Diners Club'
15
+ | 'JCB'
16
+ | 'Mir'
17
+ | 'UnionPay'
18
+ | 'Maestro'
19
+ | 'InstaPayment'
20
+ | 'Unknown';
21
+ }
22
+
23
+ export interface HoleViewConfig {
24
+ left: number; // Percentages (e.g., 10 for 10%)
25
+ top: number;
26
+ width: number;
27
+ height: number;
28
+ }
29
+
30
+ export interface CreditCardScannerProps {
31
+ /** Ref to controll camera programatically */
32
+ cameraRef?: React.RefObject<CameraRef | null> | undefined;
33
+ /** Enables or disables the camera and frame processing */
34
+ isActive: boolean;
35
+
36
+ /** Callback fired when a valid card is detected (passes Luhn check) */
37
+ onScanSuccess: (result: CardResult) => void;
38
+
39
+ /** Callback for camera initialization or permission errors */
40
+ onError?: (error: Error) => void;
41
+
42
+ /** Container style to allow full-screen or partial-screen rendering */
43
+ style?: StyleProp<ViewStyle>;
44
+
45
+ /** Optional: Customize the HoleView overlay mask */
46
+ overlayColor?: string; // Default: 'rgba(0,0,0,0.6)'
47
+
48
+ /** Optional: Customize the cutout coordinates */
49
+ holeViewConfig?: HoleViewConfig;
50
+
51
+ /** Optional: Pass down native camera props (e.g., enable torch) */
52
+ cameraProps?: Partial<CameraProps>;
53
+
54
+ /** Optional: Pass parent height to align hole view correctly with scan region */
55
+ parentViewHeight?: number;
56
+ }
@@ -0,0 +1,83 @@
1
+ import { type CardResult } from '../types';
2
+ import { getCardIssuer } from './validators';
3
+
4
+ export interface OCRBlock {
5
+ blockText: string;
6
+ }
7
+
8
+ const CARD_KEYWORDS_REGEX =
9
+ /\b(VISA|MASTERCARD|DISCOVER|AMERICAN|EXPRESS|AMEX|RUPAY|MAESTRO|CIRRUS|DEBIT|CREDIT|PREPAID|PLATINUM|GOLD|CLASSIC|SIGNATURE|INFINITE|WORLD|ELITE|BUSINESS|CORPORATE|BANK|CARD|SERVICES|FINANCIAL|TRUST|UNION|FEDERAL|NATIONAL|INTERNATIONAL|GLOBAL|PLUS|REWARDS|CASHBACK|VALID|THRU|FROM|UNTIL|EXPIRES|EXP|DATE|GOOD|MEMBER|SINCE|CVV|CVC|CID|AUTHORIZED|PROPERTY|RETURN|CUSTOMER|SERVICE|ACCOUNT|SAVING)\b/i;
10
+
11
+ function isValidNameLine(text: string) {
12
+ 'worklet';
13
+ // 1. Clean up whitespace
14
+ const cleanedText = text.trim();
15
+
16
+ // 2. Ignore if it contains any numbers (names don't have digits)
17
+ if (/\d/.test(cleanedText)) return false;
18
+
19
+ // 3. Ignore if it contains common card boilerplate keywords
20
+ const hasKeywords = CARD_KEYWORDS_REGEX.test(cleanedText);
21
+ if (hasKeywords) return false;
22
+
23
+ // 4. Ignore if it's too short to be a full name (e.g., single letters or noise)
24
+ if (cleanedText.length < 4 || cleanedText.length > 30) return false;
25
+
26
+ return true;
27
+ }
28
+
29
+ export const parseCardData = (blocks: OCRBlock[]): Partial<CardResult> => {
30
+ 'worklet';
31
+ let cardNumber: string | undefined;
32
+ let expiryDate: string | null = null;
33
+ let cardholderName: string | undefined;
34
+
35
+ // Potential name block lines
36
+ const potentialNames: string[] = [];
37
+
38
+ for (const block of blocks) {
39
+ const text = block.blockText.trim();
40
+ // Normalize string to help with matching
41
+ const normalizedText = text.replace(/[\r\n]+/g, ' ');
42
+
43
+ // 1. Extract Card Number
44
+ // Looks for 13 to 19 digits, possibly separated by spaces or dashes
45
+ if (!cardNumber) {
46
+ const cardNumMatch = normalizedText.match(/(?:\d[ -]*?){13,19}/);
47
+ if (cardNumMatch) {
48
+ const potentialNumber = cardNumMatch[0].replace(/\D/g, '');
49
+ if (potentialNumber.length >= 13 && potentialNumber.length <= 19) {
50
+ cardNumber = potentialNumber;
51
+ continue; // Skip further parsing on this block if it's the card number
52
+ }
53
+ }
54
+ }
55
+
56
+ // 2. Extract Expiry Date (MM/YY or MM/YYYY)
57
+ if (!expiryDate) {
58
+ const expiryMatch = normalizedText.match(
59
+ /\b(0[1-9]|1[0-2])\s*[\/\-]\s*(\d{2}|\d{4})\b/
60
+ );
61
+ if (expiryMatch) {
62
+ expiryDate = `${expiryMatch[1]}/${expiryMatch[2]}`;
63
+ continue;
64
+ }
65
+ }
66
+
67
+ // 3. Name heuristics
68
+ if (isValidNameLine(text)) {
69
+ potentialNames.push(text);
70
+ }
71
+ }
72
+
73
+ // Very basic heuristic for name: take the longest block that matches our criteria
74
+ if (potentialNames.length > 0) {
75
+ cardholderName = potentialNames.sort((a, b) => b.length - a.length)[0];
76
+ }
77
+ return {
78
+ cardNumber: cardNumber || '',
79
+ expiryDate,
80
+ cardholderName,
81
+ issuer: cardNumber ? getCardIssuer(cardNumber) : 'Unknown',
82
+ };
83
+ };
@@ -0,0 +1,86 @@
1
+ import { type CardResult } from '../types';
2
+
3
+ export const isValidLuhn = (cardNumber: string): boolean => {
4
+ 'worklet';
5
+ // Remove all non-digit characters
6
+ const cleanNumber = cardNumber.replace(/\D/g, '');
7
+ if (cleanNumber.length === 0) return false;
8
+
9
+ let sum = 0;
10
+ let isEven = false;
11
+
12
+ // Loop from right to left
13
+ for (let i = cleanNumber.length - 1; i >= 0; i--) {
14
+ let digit = parseInt(cleanNumber.charAt(i), 10);
15
+
16
+ if (isEven) {
17
+ digit *= 2;
18
+ if (digit > 9) {
19
+ digit -= 9;
20
+ }
21
+ }
22
+
23
+ sum += digit;
24
+ isEven = !isEven;
25
+ }
26
+
27
+ return sum % 10 === 0;
28
+ };
29
+
30
+ export const getCardIssuer = (cardNumber: string): CardResult['issuer'] => {
31
+ 'worklet';
32
+ const cleanNumber = cardNumber.replace(/\D/g, '');
33
+
34
+ // 1. VISA – must come before UnionPay (some 4* UnionPay co-brands exist)
35
+ if (/^4/.test(cleanNumber)) return 'Visa';
36
+
37
+ // 2. MASTERCARD – 51-55 OR 2221-2720
38
+ if (
39
+ /^5[1-5]/.test(cleanNumber) ||
40
+ /^(222[1-9]|22[3-9]\d|2[3-6]\d{2}|27[01]\d|2720)/.test(cleanNumber)
41
+ )
42
+ return 'Mastercard';
43
+
44
+ // 3. AMEX
45
+ if (/^3[47]/.test(cleanNumber)) return 'Amex';
46
+
47
+ // 4. DINERS CLUB – before JCB (both start with 3)
48
+ if (/^(30[0-5]|3095|36|3[89])/.test(cleanNumber)) return 'Diners Club';
49
+
50
+ // 5. JCB – 3528-3589
51
+ if (/^35(2[89]|[3-8]\d)/.test(cleanNumber)) return 'JCB';
52
+
53
+ // 6. MIR (Russia)
54
+ if (/^220[0-4]/.test(cleanNumber)) return 'Mir';
55
+
56
+ // 7. RUPAY – check before Discover (shares 60/65 prefixes)
57
+ if (
58
+ /^(508|353|356|652[12]|6[0-9]{3}(?=.*81|.*82)|81[0-9]|82[0-9])/.test(
59
+ cleanNumber
60
+ ) ||
61
+ /^(508[0-9]|6521|6522)/.test(cleanNumber) ||
62
+ /^(60[2-9]|81|82)/.test(cleanNumber)
63
+ )
64
+ return 'RuPay';
65
+
66
+ // 8. DISCOVER – 6011, 644-649, 65, 622126-622925
67
+ if (
68
+ /^6011/.test(cleanNumber) ||
69
+ /^64[4-9]/.test(cleanNumber) ||
70
+ /^65/.test(cleanNumber) ||
71
+ /^622(1(2[6-9]|[3-9]\d)|[2-8]\d{2}|9([01]\d|2[0-5]))/.test(cleanNumber)
72
+ )
73
+ return 'Discover';
74
+
75
+ // 9. CHINA UNIONPAY – broad 62 catch (after Discover's 622* check)
76
+ if (/^62/.test(cleanNumber)) return 'UnionPay';
77
+
78
+ // 10. MAESTRO
79
+ if (/^(5018|5020|5038|5893|6304|6759|676[1-3])/.test(cleanNumber))
80
+ return 'Maestro';
81
+
82
+ // 11. INSTAPAYMENT
83
+ if (/^63[7-9]/.test(cleanNumber)) return 'InstaPayment';
84
+
85
+ return 'Unknown';
86
+ };