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.
- package/LICENSE +20 -0
- package/README.md +202 -0
- package/lib/module/components/CreditCardScanner.js +66 -0
- package/lib/module/components/CreditCardScanner.js.map +1 -0
- package/lib/module/components/ScannerOverlay.js +43 -0
- package/lib/module/components/ScannerOverlay.js.map +1 -0
- package/lib/module/hooks/useCardScanner.js +53 -0
- package/lib/module/hooks/useCardScanner.js.map +1 -0
- package/lib/module/index.js +5 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/types/index.js +4 -0
- package/lib/module/types/index.js.map +1 -0
- package/lib/module/utils/parsers.js +75 -0
- package/lib/module/utils/parsers.js.map +1 -0
- package/lib/module/utils/validators.js +65 -0
- package/lib/module/utils/validators.js.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/components/CreditCardScanner.d.ts +4 -0
- package/lib/typescript/src/components/CreditCardScanner.d.ts.map +1 -0
- package/lib/typescript/src/components/ScannerOverlay.d.ts +9 -0
- package/lib/typescript/src/components/ScannerOverlay.d.ts.map +1 -0
- package/lib/typescript/src/hooks/useCardScanner.d.ts +11 -0
- package/lib/typescript/src/hooks/useCardScanner.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +3 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/types/index.d.ts +35 -0
- package/lib/typescript/src/types/index.d.ts.map +1 -0
- package/lib/typescript/src/utils/parsers.d.ts +6 -0
- package/lib/typescript/src/utils/parsers.d.ts.map +1 -0
- package/lib/typescript/src/utils/validators.d.ts +4 -0
- package/lib/typescript/src/utils/validators.d.ts.map +1 -0
- package/package.json +137 -0
- package/src/components/CreditCardScanner.tsx +63 -0
- package/src/components/ScannerOverlay.tsx +52 -0
- package/src/hooks/useCardScanner.ts +56 -0
- package/src/index.tsx +2 -0
- package/src/types/index.ts +56 -0
- package/src/utils/parsers.ts +83 -0
- 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 @@
|
|
|
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 @@
|
|
|
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 @@
|
|
|
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 @@
|
|
|
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 @@
|
|
|
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 @@
|
|
|
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,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
|
+
};
|