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
18export type BarCodePoint = {
19  x: number;
20  y: number;
21};
22
23export type BarCodeSize = {
24  height: number;
25  width: number;
26};
27
28export type BarCodeBounds = {
29  origin: BarCodePoint;
30  size: BarCodeSize;
31};
32
33export type BarCodeScannerResult = {
34  type: string;
35  data: string;
36  bounds?: BarCodeBounds;
37  cornerPoints?: BarCodePoint[];
38};
39
40export type BarCodeEvent = BarCodeScannerResult & {
41  target?: number;
42};
43
44export type BarCodeEventCallbackArguments = {
45  nativeEvent: BarCodeEvent;
46};
47
48export { PermissionResponse, PermissionStatus, PermissionHookOptions };
49
50export type BarCodeScannedCallback = (params: BarCodeEvent) => void;
51
52export interface BarCodeScannerProps extends ViewProps {
53  type?: 'front' | 'back' | number;
54  barCodeTypes?: string[];
55  onBarCodeScanned?: BarCodeScannedCallback;
56}
57
58export class BarCodeScanner extends React.Component<BarCodeScannerProps> {
59  lastEvents: { [key: string]: any } = {};
60  lastEventsTimes: { [key: string]: any } = {};
61
62  static Constants = {
63    BarCodeType,
64    Type,
65  };
66
67  static ConversionTables = {
68    type: Type,
69  };
70
71  static defaultProps = {
72    type: Type.back,
73    barCodeTypes: Object.values(BarCodeType),
74  };
75
76  static async getPermissionsAsync(): Promise<PermissionResponse> {
77    return ExpoBarCodeScannerModule.getPermissionsAsync();
78  }
79
80  static async requestPermissionsAsync(): Promise<PermissionResponse> {
81    return ExpoBarCodeScannerModule.requestPermissionsAsync();
82  }
83
84  // @needsAudit
85  /**
86   * Check or request permissions for the barcode scanner.
87   * This uses both `requestPermissionAsync` and `getPermissionsAsync` to interact with the permissions.
88   *
89   * @example
90   * ```ts
91   * const [status, requestPermission] = BarCodeScanner.usePermissions();
92   * ```
93   */
94  static usePermissions = createPermissionHook({
95    getMethod: BarCodeScanner.getPermissionsAsync,
96    requestMethod: BarCodeScanner.requestPermissionsAsync,
97  });
98
99  static async scanFromURLAsync(
100    url: string,
101    barCodeTypes: string[] = Object.values(BarCodeType)
102  ): Promise<BarCodeScannerResult[]> {
103    if (!ExpoBarCodeScannerModule.scanFromURLAsync) {
104      throw new UnavailabilityError('expo-barcode-scanner', 'scanFromURLAsync');
105    }
106    if (Array.isArray(barCodeTypes) && !barCodeTypes.length) {
107      throw new Error('No barCodeTypes specified; provide at least one barCodeType for scanner');
108    }
109
110    if (Platform.OS === 'ios') {
111      if (Array.isArray(barCodeTypes) && !barCodeTypes.includes(BarCodeType.qr)) {
112        // Only QR type is supported on iOS, fail if one tries to use other types
113        throw new Error('Only QR type is supported by scanFromURLAsync() on iOS');
114      }
115      // on iOS use only supported QR type
116      return await ExpoBarCodeScannerModule.scanFromURLAsync(url, [BarCodeType.qr]);
117    }
118
119    // On other platforms, if barCodeTypes is not provided, use all available types
120    return await ExpoBarCodeScannerModule.scanFromURLAsync(url, barCodeTypes);
121  }
122
123  render() {
124    const nativeProps = this.convertNativeProps(this.props);
125    const { onBarCodeScanned } = this.props;
126    return (
127      <ExpoBarCodeScannerView
128        {...nativeProps}
129        onBarCodeScanned={this.onObjectDetected(onBarCodeScanned)}
130      />
131    );
132  }
133
134  onObjectDetected =
135    (callback?: BarCodeScannedCallback) =>
136    ({ nativeEvent }: BarCodeEventCallbackArguments) => {
137      const { type } = nativeEvent;
138      if (
139        this.lastEvents[type] &&
140        this.lastEventsTimes[type] &&
141        JSON.stringify(nativeEvent) === this.lastEvents[type] &&
142        Date.now() - this.lastEventsTimes[type] < EVENT_THROTTLE_MS
143      ) {
144        return;
145      }
146
147      if (callback) {
148        callback(nativeEvent);
149        this.lastEventsTimes[type] = new Date();
150        this.lastEvents[type] = JSON.stringify(nativeEvent);
151      }
152    };
153
154  convertNativeProps(props: BarCodeScannerProps) {
155    const nativeProps: BarCodeScannerProps = {};
156
157    for (const [key, value] of Object.entries(props)) {
158      if (typeof value === 'string' && BarCodeScanner.ConversionTables[key]) {
159        nativeProps[key] = BarCodeScanner.ConversionTables[key][value];
160      } else {
161        nativeProps[key] = value;
162      }
163    }
164
165    return nativeProps;
166  }
167}
168
169export const { Constants, getPermissionsAsync, requestPermissionsAsync, usePermissions } =
170  BarCodeScanner;
171