1import {
2  PermissionResponse,
3  PermissionStatus,
4  PermissionHookOptions,
5  createPermissionHook,
6  UnavailabilityError,
7} from 'expo-modules-core';
8import * as React from 'react';
9import { Platform, ViewProps } from 'react-native';
10
11import ExpoBarCodeScannerModule from './ExpoBarCodeScannerModule';
12import ExpoBarCodeScannerView from './ExpoBarCodeScannerView';
13
14const { BarCodeType, Type } = ExpoBarCodeScannerModule;
15
16const EVENT_THROTTLE_MS = 500;
17
18// @needsAudit
19/**
20 * Those coordinates are represented in the coordinate space of the barcode source (e.g. when you
21 * are using the barcode scanner view, these values are adjusted to the dimensions of the view).
22 */
23export type BarCodePoint = {
24  /**
25   * The `x` coordinate value.
26   */
27  x: number;
28  /**
29   * The `y` coordinate value.
30   */
31  y: number;
32};
33
34// @needsAudit
35export type BarCodeSize = {
36  /**
37   * The height value.
38   */
39  height: number;
40  /**
41   * The width value.
42   */
43  width: number;
44};
45
46// @needsAudit
47export type BarCodeBounds = {
48  /**
49   * The origin point of the bounding box.
50   */
51  origin: BarCodePoint;
52  /**
53   * The size of the bounding box.
54   */
55  size: BarCodeSize;
56};
57
58// @needsAudit
59export type BarCodeScannerResult = {
60  /**
61   * The barcode type.
62   */
63  type: string;
64  /**
65   * The information encoded in the bar code.
66   */
67  data: string;
68  /**
69   * The [BarCodeBounds](#barcodebounds) object.
70   * `bounds` in some case will be representing an empty rectangle.
71   * Moreover, `bounds` doesn't have to bound the whole barcode.
72   * For some types, they will represent the area used by the scanner.
73   */
74  bounds: BarCodeBounds;
75  /**
76   * Corner points of the bounding box.
77   * `cornerPoints` is not always available and may be empty. On iOS, for `code39` and `pdf417`
78   * you don't get this value.
79   */
80  cornerPoints: BarCodePoint[];
81};
82
83// @docsMissing
84export type BarCodeEvent = BarCodeScannerResult & {
85  target?: number;
86};
87
88// @docsMissing
89export type BarCodeEventCallbackArguments = {
90  nativeEvent: BarCodeEvent;
91};
92
93// @docsMissing
94export type BarCodeScannedCallback = (params: BarCodeEvent) => void;
95
96// @needsAudit
97export type BarCodeScannerProps = ViewProps & {
98  /**
99   * Camera facing. Use one of `BarCodeScanner.Constants.Type`. Use either `Type.front` or `Type.back`.
100   * Same as `Camera.Constants.Type`.
101   * @default Type.back
102   */
103  type?: 'front' | 'back' | number;
104  /**
105   * An array of bar code types. Usage: `BarCodeScanner.Constants.BarCodeType.<codeType>` where
106   * `codeType` is one of these [listed above](#supported-formats). Defaults to all supported bar
107   * code types. It is recommended to provide only the bar code formats you expect to scan to
108   * minimize battery usage.
109   *
110   * For example: `barCodeTypes={[BarCodeScanner.Constants.BarCodeType.qr]}`.
111   */
112  barCodeTypes?: string[];
113  /**
114   * A callback that is invoked when a bar code has been successfully scanned. The callback is
115   * provided with an [BarCodeScannerResult](#barcodescannerresult).
116   * > __Note:__ Passing `undefined` to the `onBarCodeScanned` prop will result in no scanning. This
117   * > can be used to effectively "pause" the scanner so that it doesn't continually scan even after
118   * > data has been retrieved.
119   */
120  onBarCodeScanned?: BarCodeScannedCallback;
121};
122
123export class BarCodeScanner extends React.Component<BarCodeScannerProps> {
124  lastEvents: { [key: string]: any } = {};
125  lastEventsTimes: { [key: string]: any } = {};
126
127  static Constants = {
128    BarCodeType,
129    Type,
130  };
131
132  static ConversionTables = {
133    type: Type,
134  };
135
136  static defaultProps = {
137    type: Type.back,
138    barCodeTypes: Object.values(BarCodeType),
139  };
140
141  // @needsAudit
142  /**
143   * Checks user's permissions for accessing the camera.
144   * @return Return a promise that fulfills to an object of type [`PermissionResponse`](#permissionresponse).
145   */
146  static async getPermissionsAsync(): Promise<PermissionResponse> {
147    return ExpoBarCodeScannerModule.getPermissionsAsync();
148  }
149
150  // @needsAudit
151  /**
152   * Asks the user to grant permissions for accessing the camera.
153   *
154   * On iOS this will require apps to specify the `NSCameraUsageDescription` entry in the `Info.plist`.
155   * @return Return a promise that fulfills to an object of type [`PermissionResponse`](#permissionresponse).
156   */
157  static async requestPermissionsAsync(): Promise<PermissionResponse> {
158    return ExpoBarCodeScannerModule.requestPermissionsAsync();
159  }
160
161  // @needsAudit
162  /**
163   * Check or request permissions for the barcode scanner.
164   * This uses both `requestPermissionAsync` and `getPermissionsAsync` to interact with the permissions.
165   *
166   * @example
167   * ```ts
168   * const [permissionResponse, requestPermission] = BarCodeScanner.usePermissions();
169   * ```
170   */
171  static usePermissions = createPermissionHook({
172    getMethod: BarCodeScanner.getPermissionsAsync,
173    requestMethod: BarCodeScanner.requestPermissionsAsync,
174  });
175
176  // @needsAudit
177  /**
178   * Scan bar codes from the image given by the URL.
179   * @param url URL to get the image from.
180   * @param barCodeTypes An array of bar code types. Defaults to all supported bar code types on
181   * the platform.
182   * > __Note:__ Only QR codes are supported on iOS.
183   * @return A possibly empty array of objects of the `BarCodeScannerResult` shape, where the type
184   * refers to the bar code type that was scanned and the data is the information encoded in the bar
185   * code.
186   */
187  static async scanFromURLAsync(
188    url: string,
189    barCodeTypes: string[] = Object.values(BarCodeType)
190  ): Promise<BarCodeScannerResult[]> {
191    if (!ExpoBarCodeScannerModule.scanFromURLAsync) {
192      throw new UnavailabilityError('expo-barcode-scanner', 'scanFromURLAsync');
193    }
194    if (Array.isArray(barCodeTypes) && !barCodeTypes.length) {
195      throw new Error('No barCodeTypes specified; provide at least one barCodeType for scanner');
196    }
197
198    if (Platform.OS === 'ios') {
199      if (Array.isArray(barCodeTypes) && !barCodeTypes.includes(BarCodeType.qr)) {
200        // Only QR type is supported on iOS, fail if one tries to use other types
201        throw new Error('Only QR type is supported by scanFromURLAsync() on iOS');
202      }
203      // on iOS use only supported QR type
204      return await ExpoBarCodeScannerModule.scanFromURLAsync(url, [BarCodeType.qr]);
205    }
206
207    // On other platforms, if barCodeTypes is not provided, use all available types
208    return await ExpoBarCodeScannerModule.scanFromURLAsync(url, barCodeTypes);
209  }
210
211  render() {
212    const nativeProps = this.convertNativeProps(this.props);
213    const { onBarCodeScanned } = this.props;
214    return (
215      <ExpoBarCodeScannerView
216        {...nativeProps}
217        onBarCodeScanned={this.onObjectDetected(onBarCodeScanned)}
218      />
219    );
220  }
221
222  /**
223   * @hidden
224   */
225  onObjectDetected =
226    (callback?: BarCodeScannedCallback) =>
227    ({ nativeEvent }: BarCodeEventCallbackArguments) => {
228      const { type } = nativeEvent;
229      if (
230        this.lastEvents[type] &&
231        this.lastEventsTimes[type] &&
232        JSON.stringify(nativeEvent) === this.lastEvents[type] &&
233        Date.now() - this.lastEventsTimes[type] < EVENT_THROTTLE_MS
234      ) {
235        return;
236      }
237
238      if (callback) {
239        callback(nativeEvent);
240        this.lastEventsTimes[type] = new Date();
241        this.lastEvents[type] = JSON.stringify(nativeEvent);
242      }
243    };
244
245  /**
246   * @hidden
247   */
248  convertNativeProps(props: BarCodeScannerProps) {
249    const nativeProps: BarCodeScannerProps = {};
250
251    for (const [key, value] of Object.entries(props)) {
252      if (typeof value === 'string' && BarCodeScanner.ConversionTables[key]) {
253        nativeProps[key] = BarCodeScanner.ConversionTables[key][value];
254      } else {
255        nativeProps[key] = value;
256      }
257    }
258
259    return nativeProps;
260  }
261}
262
263export { PermissionResponse, PermissionStatus, PermissionHookOptions };
264export const { Constants, getPermissionsAsync, requestPermissionsAsync, scanFromURLAsync } =
265  BarCodeScanner;
266