@unif/react-native-hms-scan 0.1.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 (111) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +149 -0
  3. package/ReactNativeHmsScan.podspec +30 -0
  4. package/android/build.gradle +79 -0
  5. package/android/consumer-rules.pro +13 -0
  6. package/android/src/main/AndroidManifest.xml +23 -0
  7. package/android/src/main/java/com/unif/reactnativehmsscan/HmsScanModule.kt +192 -0
  8. package/android/src/main/java/com/unif/reactnativehmsscan/HmsScanResultMapper.kt +145 -0
  9. package/android/src/main/java/com/unif/reactnativehmsscan/HmsScanView.kt +257 -0
  10. package/android/src/main/java/com/unif/reactnativehmsscan/HmsScanViewManager.kt +66 -0
  11. package/android/src/main/java/com/unif/reactnativehmsscan/ReactNativeHmsScanPackage.kt +49 -0
  12. package/ios/HmsScanModule.h +20 -0
  13. package/ios/HmsScanModule.mm +161 -0
  14. package/ios/HmsScanResultMapper.h +45 -0
  15. package/ios/HmsScanResultMapper.mm +297 -0
  16. package/ios/HmsScanView.h +24 -0
  17. package/ios/HmsScanView.mm +322 -0
  18. package/lib/module/HmsScanView.js +43 -0
  19. package/lib/module/HmsScanView.js.map +1 -0
  20. package/lib/module/HmsScanViewNativeComponent.ts +44 -0
  21. package/lib/module/NativeHmsScan.js +11 -0
  22. package/lib/module/NativeHmsScan.js.map +1 -0
  23. package/lib/module/Scanner/DeniedOverlay.js +60 -0
  24. package/lib/module/Scanner/DeniedOverlay.js.map +1 -0
  25. package/lib/module/Scanner/ResultFail.js +79 -0
  26. package/lib/module/Scanner/ResultFail.js.map +1 -0
  27. package/lib/module/Scanner/ResultFocus.js +174 -0
  28. package/lib/module/Scanner/ResultFocus.js.map +1 -0
  29. package/lib/module/Scanner/ScanHint.js +44 -0
  30. package/lib/module/Scanner/ScanHint.js.map +1 -0
  31. package/lib/module/Scanner/ScanToolbar.js +103 -0
  32. package/lib/module/Scanner/ScanToolbar.js.map +1 -0
  33. package/lib/module/Scanner/ScanTopBar.js +87 -0
  34. package/lib/module/Scanner/ScanTopBar.js.map +1 -0
  35. package/lib/module/Scanner/Scanner.js +184 -0
  36. package/lib/module/Scanner/Scanner.js.map +1 -0
  37. package/lib/module/Scanner/Viewfinder.js +159 -0
  38. package/lib/module/Scanner/Viewfinder.js.map +1 -0
  39. package/lib/module/Scanner/scanChrome.js +23 -0
  40. package/lib/module/Scanner/scanChrome.js.map +1 -0
  41. package/lib/module/decodeImage.js +32 -0
  42. package/lib/module/decodeImage.js.map +1 -0
  43. package/lib/module/format.js +66 -0
  44. package/lib/module/format.js.map +1 -0
  45. package/lib/module/index.js +21 -0
  46. package/lib/module/index.js.map +1 -0
  47. package/lib/module/mock.js +30 -0
  48. package/lib/module/mock.js.map +1 -0
  49. package/lib/module/package.json +1 -0
  50. package/lib/module/permissions.js +28 -0
  51. package/lib/module/permissions.js.map +1 -0
  52. package/lib/module/types.js +46 -0
  53. package/lib/module/types.js.map +1 -0
  54. package/lib/typescript/jest.setup.d.ts +1 -0
  55. package/lib/typescript/jest.setup.d.ts.map +1 -0
  56. package/lib/typescript/package.json +1 -0
  57. package/lib/typescript/src/HmsScanView.d.ts +36 -0
  58. package/lib/typescript/src/HmsScanView.d.ts.map +1 -0
  59. package/lib/typescript/src/HmsScanViewNativeComponent.d.ts +32 -0
  60. package/lib/typescript/src/HmsScanViewNativeComponent.d.ts.map +1 -0
  61. package/lib/typescript/src/NativeHmsScan.d.ts +17 -0
  62. package/lib/typescript/src/NativeHmsScan.d.ts.map +1 -0
  63. package/lib/typescript/src/Scanner/DeniedOverlay.d.ts +7 -0
  64. package/lib/typescript/src/Scanner/DeniedOverlay.d.ts.map +1 -0
  65. package/lib/typescript/src/Scanner/ResultFail.d.ts +7 -0
  66. package/lib/typescript/src/Scanner/ResultFail.d.ts.map +1 -0
  67. package/lib/typescript/src/Scanner/ResultFocus.d.ts +12 -0
  68. package/lib/typescript/src/Scanner/ResultFocus.d.ts.map +1 -0
  69. package/lib/typescript/src/Scanner/ScanHint.d.ts +8 -0
  70. package/lib/typescript/src/Scanner/ScanHint.d.ts.map +1 -0
  71. package/lib/typescript/src/Scanner/ScanToolbar.d.ts +9 -0
  72. package/lib/typescript/src/Scanner/ScanToolbar.d.ts.map +1 -0
  73. package/lib/typescript/src/Scanner/ScanTopBar.d.ts +10 -0
  74. package/lib/typescript/src/Scanner/ScanTopBar.d.ts.map +1 -0
  75. package/lib/typescript/src/Scanner/Scanner.d.ts +34 -0
  76. package/lib/typescript/src/Scanner/Scanner.d.ts.map +1 -0
  77. package/lib/typescript/src/Scanner/Viewfinder.d.ts +11 -0
  78. package/lib/typescript/src/Scanner/Viewfinder.d.ts.map +1 -0
  79. package/lib/typescript/src/Scanner/scanChrome.d.ts +19 -0
  80. package/lib/typescript/src/Scanner/scanChrome.d.ts.map +1 -0
  81. package/lib/typescript/src/decodeImage.d.ts +12 -0
  82. package/lib/typescript/src/decodeImage.d.ts.map +1 -0
  83. package/lib/typescript/src/format.d.ts +13 -0
  84. package/lib/typescript/src/format.d.ts.map +1 -0
  85. package/lib/typescript/src/index.d.ts +10 -0
  86. package/lib/typescript/src/index.d.ts.map +1 -0
  87. package/lib/typescript/src/mock.d.ts +12 -0
  88. package/lib/typescript/src/mock.d.ts.map +1 -0
  89. package/lib/typescript/src/permissions.d.ts +9 -0
  90. package/lib/typescript/src/permissions.d.ts.map +1 -0
  91. package/lib/typescript/src/types.d.ts +62 -0
  92. package/lib/typescript/src/types.d.ts.map +1 -0
  93. package/package.json +174 -0
  94. package/src/HmsScanView.tsx +87 -0
  95. package/src/HmsScanViewNativeComponent.ts +44 -0
  96. package/src/NativeHmsScan.ts +24 -0
  97. package/src/Scanner/DeniedOverlay.tsx +59 -0
  98. package/src/Scanner/ResultFail.tsx +71 -0
  99. package/src/Scanner/ResultFocus.tsx +118 -0
  100. package/src/Scanner/ScanHint.tsx +38 -0
  101. package/src/Scanner/ScanToolbar.tsx +93 -0
  102. package/src/Scanner/ScanTopBar.tsx +74 -0
  103. package/src/Scanner/Scanner.tsx +238 -0
  104. package/src/Scanner/Viewfinder.tsx +115 -0
  105. package/src/Scanner/scanChrome.ts +20 -0
  106. package/src/decodeImage.ts +36 -0
  107. package/src/format.ts +93 -0
  108. package/src/index.tsx +35 -0
  109. package/src/mock.ts +43 -0
  110. package/src/permissions.ts +27 -0
  111. package/src/types.ts +138 -0
@@ -0,0 +1,297 @@
1
+ //
2
+ // HmsScanResultMapper.mm
3
+ // @unif/react-native-hms-scan
4
+ //
5
+
6
+ #import "HmsScanResultMapper.h"
7
+
8
+ #import <ScanKitFrameWork/ScanKitFrameWork.h>
9
+
10
+ // NOTE on naming collisions: HMSScanFormatTypeCode (in HmsScanFormat.h) declares
11
+ // *global, unscoped* enum constants named QR_CODE, EAN_13, DATA_MATRIX, etc.
12
+ // Those identifiers therefore refer to integers, NOT to our output strings.
13
+ // We only ever compare against those enum constants and emit string *literals*
14
+ // (@"QR_CODE" ...), so there is no clash.
15
+
16
+ @implementation HmsScanResultMapper
17
+
18
+ #pragma mark - CSV -> scanFormatType bitmask
19
+
20
+ // Maps one of our BarcodeFormat tokens to the matching HMSScanFormatTypeCode bit.
21
+ // Returns 0 for tokens HUAWEI iOS does not support (e.g. MULTI_FUNCTIONAL) or
22
+ // UNKNOWN, so they contribute nothing to the OR-ed mask.
23
+ + (unsigned int)bitForFormatToken:(NSString *)token
24
+ {
25
+ NSString *t = [[token stringByTrimmingCharactersInSet:
26
+ [NSCharacterSet whitespaceAndNewlineCharacterSet]] uppercaseString];
27
+ if (t.length == 0) {
28
+ return 0;
29
+ }
30
+
31
+ static NSDictionary<NSString *, NSNumber *> *map = nil;
32
+ static dispatch_once_t onceToken;
33
+ dispatch_once(&onceToken, ^{
34
+ map = @{
35
+ @"QR_CODE": @(QR_CODE),
36
+ @"AZTEC": @(AZTEC),
37
+ @"DATA_MATRIX": @(DATA_MATRIX),
38
+ @"PDF417": @(PDF_417),
39
+ @"CODABAR": @(CODABAR),
40
+ @"CODE_39": @(CODE_39),
41
+ @"CODE_93": @(CODE_93),
42
+ @"CODE_128": @(CODE_128),
43
+ @"EAN_8": @(EAN_8),
44
+ @"EAN_13": @(EAN_13),
45
+ @"UPC_A": @(UPC_A),
46
+ @"UPC_E": @(UPC_E),
47
+ // Our ITF14 maps to HUAWEI's ITF bit.
48
+ @"ITF14": @(ITF),
49
+ // MULTI_FUNCTIONAL / UNKNOWN have no HUAWEI iOS equivalent -> 0.
50
+ };
51
+ });
52
+
53
+ NSNumber *bit = map[t];
54
+ return bit ? (unsigned int)bit.unsignedIntValue : 0u;
55
+ }
56
+
57
+ + (unsigned int)scanFormatTypeFromCsv:(NSString *)csv
58
+ {
59
+ NSString *trimmed = [csv stringByTrimmingCharactersInSet:
60
+ [NSCharacterSet whitespaceAndNewlineCharacterSet]];
61
+ if (trimmed.length == 0) {
62
+ return (unsigned int)ALL; // empty string == all formats
63
+ }
64
+
65
+ unsigned int mask = 0u;
66
+ for (NSString *token in [trimmed componentsSeparatedByString:@","]) {
67
+ mask |= [self bitForFormatToken:token];
68
+ }
69
+ // If nothing resolved (e.g. only MULTI_FUNCTIONAL/UNKNOWN was requested),
70
+ // fall back to ALL so we still attempt a decode rather than scanning nothing.
71
+ return mask == 0u ? (unsigned int)ALL : mask;
72
+ }
73
+
74
+ #pragma mark - formatValue -> our BarcodeFormat string
75
+
76
+ // HUAWEI's `formatValue` is documented as the HMSScanFormatTypeCode integer.
77
+ // We tolerate it being delivered as an NSNumber (bit value) OR an NSString
78
+ // (either a numeric string or already a token like "QR_CODE"), since the
79
+ // public headers do not pin the type and demos have shown both shapes.
80
+ + (NSString *)formatStringFromValue:(id)formatValue
81
+ {
82
+ // Case 1: numeric (NSNumber, or numeric NSString) -> match the enum bit.
83
+ NSInteger code = NSNotFound;
84
+ if ([formatValue isKindOfClass:[NSNumber class]]) {
85
+ code = [(NSNumber *)formatValue integerValue];
86
+ } else if ([formatValue isKindOfClass:[NSString class]]) {
87
+ NSString *s = (NSString *)formatValue;
88
+ NSString *trimmed = [s stringByTrimmingCharactersInSet:
89
+ [NSCharacterSet whitespaceAndNewlineCharacterSet]];
90
+ // Pure-integer string?
91
+ NSScanner *scanner = [NSScanner scannerWithString:trimmed];
92
+ NSInteger parsed = 0;
93
+ if ([scanner scanInteger:&parsed] && scanner.isAtEnd) {
94
+ code = parsed;
95
+ } else {
96
+ // Case 2: already a token string -> normalize known spellings.
97
+ NSString *upper = [trimmed uppercaseString];
98
+ if ([upper isEqualToString:@"PDF_417"]) return @"PDF417";
99
+ if ([upper isEqualToString:@"ITF"]) return @"ITF14";
100
+ static NSSet<NSString *> *known = nil;
101
+ static dispatch_once_t onceToken;
102
+ dispatch_once(&onceToken, ^{
103
+ known = [NSSet setWithArray:@[
104
+ @"QR_CODE", @"AZTEC", @"DATA_MATRIX", @"PDF417", @"CODABAR",
105
+ @"CODE_39", @"CODE_93", @"CODE_128", @"EAN_8", @"EAN_13",
106
+ @"UPC_A", @"UPC_E", @"ITF14", @"MULTI_FUNCTIONAL",
107
+ ]];
108
+ });
109
+ return [known containsObject:upper] ? upper : @"UNKNOWN";
110
+ }
111
+ } else {
112
+ return @"UNKNOWN";
113
+ }
114
+
115
+ // Map enum bit -> our string. Compare against the global enum constants.
116
+ switch (code) {
117
+ case QR_CODE: return @"QR_CODE";
118
+ case AZTEC: return @"AZTEC";
119
+ case DATA_MATRIX: return @"DATA_MATRIX";
120
+ case PDF_417: return @"PDF417";
121
+ case CODABAR: return @"CODABAR";
122
+ case CODE_39: return @"CODE_39";
123
+ case CODE_93: return @"CODE_93";
124
+ case CODE_128: return @"CODE_128";
125
+ case EAN_8: return @"EAN_8";
126
+ case EAN_13: return @"EAN_13";
127
+ case UPC_A: return @"UPC_A";
128
+ case UPC_E: return @"UPC_E";
129
+ case ITF: return @"ITF14";
130
+ default: return @"UNKNOWN";
131
+ }
132
+ }
133
+
134
+ #pragma mark - sceneType -> our BarcodeContentType (best effort)
135
+
136
+ // HUAWEI iOS does not publish a sceneType enum in the headers; precision is
137
+ // limited. We map a few well-known scene tokens/codes and otherwise return nil
138
+ // so `contentType` is omitted (the JS contract treats unknown as undefined).
139
+ + (nullable NSString *)contentTypeFromSceneType:(id)sceneType
140
+ {
141
+ NSString *token = nil;
142
+ if ([sceneType isKindOfClass:[NSString class]]) {
143
+ token = [[(NSString *)sceneType stringByTrimmingCharactersInSet:
144
+ [NSCharacterSet whitespaceAndNewlineCharacterSet]] uppercaseString];
145
+ } else if ([sceneType isKindOfClass:[NSNumber class]]) {
146
+ // No documented numeric scene mapping -> leave unknown.
147
+ return nil;
148
+ } else {
149
+ return nil;
150
+ }
151
+ if (token.length == 0) {
152
+ return nil;
153
+ }
154
+
155
+ static NSDictionary<NSString *, NSString *> *map = nil;
156
+ static dispatch_once_t onceToken;
157
+ dispatch_once(&onceToken, ^{
158
+ map = @{
159
+ @"TEXT": @"TEXT",
160
+ @"URL": @"URL",
161
+ @"URI": @"URL",
162
+ @"EMAIL": @"EMAIL",
163
+ @"PHONE": @"PHONE",
164
+ @"TEL": @"PHONE",
165
+ @"SMS": @"SMS",
166
+ @"WIFI": @"WIFI",
167
+ @"CONTACT": @"CONTACT",
168
+ @"CONTACT_INFO": @"CONTACT",
169
+ @"EVENT": @"EVENT",
170
+ @"LOCATION": @"LOCATION",
171
+ @"GEO": @"LOCATION",
172
+ @"DRIVER": @"DRIVER",
173
+ @"ISBN": @"ISBN",
174
+ @"ARTICLE": @"ARTICLE",
175
+ @"PRODUCT": @"ARTICLE",
176
+ };
177
+ });
178
+ return map[token]; // nil when unrecognized -> contentType omitted
179
+ }
180
+
181
+ #pragma mark - ResultPoint -> cornerPoints
182
+
183
+ + (nullable NSNumber *)numberFromPointDict:(NSDictionary *)dict keys:(NSArray<NSString *> *)keys
184
+ {
185
+ for (NSString *key in keys) {
186
+ id v = dict[key];
187
+ if ([v isKindOfClass:[NSNumber class]]) {
188
+ return (NSNumber *)v;
189
+ }
190
+ if ([v isKindOfClass:[NSString class]]) {
191
+ return @([(NSString *)v doubleValue]);
192
+ }
193
+ }
194
+ return nil;
195
+ }
196
+
197
+ // HUAWEI delivers `ResultPoint` as an array of dicts with `posX`/`posY` numbers.
198
+ // We tolerate a couple of key spellings just in case.
199
+ + (nullable NSArray<NSDictionary *> *)cornerPointsFromResultPoint:(id)resultPoint
200
+ {
201
+ if (![resultPoint isKindOfClass:[NSArray class]]) {
202
+ return nil;
203
+ }
204
+ NSMutableArray<NSDictionary *> *points = [NSMutableArray array];
205
+ for (id element in (NSArray *)resultPoint) {
206
+ if (![element isKindOfClass:[NSDictionary class]]) {
207
+ continue;
208
+ }
209
+ NSDictionary *p = (NSDictionary *)element;
210
+ NSNumber *x = [self numberFromPointDict:p keys:@[ @"posX", @"x", @"X" ]];
211
+ NSNumber *y = [self numberFromPointDict:p keys:@[ @"posY", @"y", @"Y" ]];
212
+ if (x != nil && y != nil) {
213
+ [points addObject:@{ @"x": x, @"y": y }];
214
+ }
215
+ }
216
+ return points.count > 0 ? [points copy] : nil;
217
+ }
218
+
219
+ #pragma mark - Dict -> ScanResult
220
+
221
+ + (nullable NSString *)valueFromHuaweiDict:(NSDictionary *)dict
222
+ {
223
+ // Primary key is `text`; tolerate a couple of alternates defensively.
224
+ for (NSString *key in @[ @"text", @"originalValue", @"showText", @"value" ]) {
225
+ id v = dict[key];
226
+ if ([v isKindOfClass:[NSString class]] && ((NSString *)v).length > 0) {
227
+ return (NSString *)v;
228
+ }
229
+ }
230
+ return nil;
231
+ }
232
+
233
+ + (nullable NSDictionary *)scanResultFromHuaweiDict:(NSDictionary *)dict
234
+ {
235
+ if (![dict isKindOfClass:[NSDictionary class]]) {
236
+ return nil;
237
+ }
238
+
239
+ NSString *value = [self valueFromHuaweiDict:dict];
240
+ if (value == nil) {
241
+ return nil; // no usable decoded text -> drop
242
+ }
243
+
244
+ NSMutableDictionary *out = [NSMutableDictionary dictionary];
245
+ out[@"value"] = value;
246
+ out[@"format"] = [self formatStringFromValue:dict[@"formatValue"]];
247
+
248
+ NSString *contentType = [self contentTypeFromSceneType:dict[@"sceneType"]];
249
+ if (contentType != nil) {
250
+ out[@"contentType"] = contentType;
251
+ }
252
+
253
+ NSArray<NSDictionary *> *corners = [self cornerPointsFromResultPoint:dict[@"ResultPoint"]];
254
+ if (corners != nil) {
255
+ out[@"cornerPoints"] = corners;
256
+ }
257
+
258
+ return [out copy];
259
+ }
260
+
261
+ + (NSArray<NSDictionary *> *)scanResultsFromHuaweiArray:(NSArray *)array
262
+ {
263
+ NSMutableArray<NSDictionary *> *results = [NSMutableArray array];
264
+ if ([array isKindOfClass:[NSArray class]]) {
265
+ for (id element in array) {
266
+ NSDictionary *mapped = [self scanResultFromHuaweiDict:element];
267
+ if (mapped != nil) {
268
+ [results addObject:mapped];
269
+ }
270
+ }
271
+ }
272
+ return [results copy];
273
+ }
274
+
275
+ #pragma mark - JSON
276
+
277
+ + (NSString *)jsonStringFromScanResults:(NSArray<NSDictionary *> *)results
278
+ {
279
+ NSArray *safe = [results isKindOfClass:[NSArray class]] ? results : @[];
280
+ if (![NSJSONSerialization isValidJSONObject:safe]) {
281
+ return @"[]";
282
+ }
283
+ NSError *error = nil;
284
+ NSData *data = [NSJSONSerialization dataWithJSONObject:safe options:0 error:&error];
285
+ if (data == nil || error != nil) {
286
+ return @"[]";
287
+ }
288
+ NSString *json = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
289
+ return json ?: @"[]";
290
+ }
291
+
292
+ + (NSString *)jsonStringFromHuaweiArray:(NSArray *)array
293
+ {
294
+ return [self jsonStringFromScanResults:[self scanResultsFromHuaweiArray:array]];
295
+ }
296
+
297
+ @end
@@ -0,0 +1,24 @@
1
+ //
2
+ // HmsScanView.h
3
+ // @unif/react-native-hms-scan
4
+ //
5
+ // Fabric component registered under the name "HmsScanView". Hosts HUAWEI Scan
6
+ // Kit's HmsCustomScanViewController as an embedded child view controller and
7
+ // bridges its delegate callbacks to the codegen events
8
+ // (onScanResult / onScanError / onTorchStatus).
9
+ //
10
+
11
+ #import <React/RCTViewComponentView.h>
12
+ #import <UIKit/UIKit.h>
13
+
14
+ #ifndef HmsScanViewNativeComponent_h
15
+ #define HmsScanViewNativeComponent_h
16
+
17
+ NS_ASSUME_NONNULL_BEGIN
18
+
19
+ @interface HmsScanView : RCTViewComponentView
20
+ @end
21
+
22
+ NS_ASSUME_NONNULL_END
23
+
24
+ #endif /* HmsScanViewNativeComponent_h */
@@ -0,0 +1,322 @@
1
+ //
2
+ // HmsScanView.mm
3
+ // @unif/react-native-hms-scan
4
+ //
5
+
6
+ #import "HmsScanView.h"
7
+ #import "HmsScanResultMapper.h"
8
+
9
+ #import <React/RCTConversions.h>
10
+
11
+ #import <react/renderer/components/ReactNativeHmsScanSpec/ComponentDescriptors.h>
12
+ #import <react/renderer/components/ReactNativeHmsScanSpec/EventEmitters.h>
13
+ #import <react/renderer/components/ReactNativeHmsScanSpec/Props.h>
14
+ #import <react/renderer/components/ReactNativeHmsScanSpec/RCTComponentViewHelpers.h>
15
+
16
+ #import "RCTFabricComponentsPlugins.h"
17
+
18
+ #import <AVFoundation/AVFoundation.h>
19
+ #import <ScanKitFrameWork/ScanKitFrameWork.h>
20
+
21
+ using namespace facebook::react;
22
+
23
+ @interface HmsScanView () <CustomizedScanDelegate>
24
+ @end
25
+
26
+ @implementation HmsScanView {
27
+ HmsCustomScanViewController *_scanVC;
28
+ // Mirror of the props we apply imperatively, so updateProps can diff against
29
+ // the *actually applied* state (RCTViewComponentView resets _props each call).
30
+ NSString *_appliedFormatsCsv;
31
+ BOOL _appliedContinuous;
32
+ BOOL _appliedPaused;
33
+ BOOL _appliedTorch;
34
+ BOOL _hasAppliedOnce;
35
+ BOOL _childAttached;
36
+ }
37
+
38
+ #pragma mark - Codegen wiring
39
+
40
+ + (ComponentDescriptorProvider)componentDescriptorProvider
41
+ {
42
+ return concreteComponentDescriptorProvider<HmsScanViewComponentDescriptor>();
43
+ }
44
+
45
+ - (instancetype)initWithFrame:(CGRect)frame
46
+ {
47
+ if (self = [super initWithFrame:frame]) {
48
+ static const auto defaultProps = std::make_shared<const HmsScanViewProps>();
49
+ _props = defaultProps;
50
+
51
+ _appliedContinuous = YES; // matches WithDefault<boolean, true>
52
+ _appliedPaused = NO;
53
+ _appliedTorch = NO;
54
+ _appliedFormatsCsv = @""; // matches WithDefault<string, ''> (= all formats)
55
+ _hasAppliedOnce = NO;
56
+ _childAttached = NO;
57
+
58
+ [self buildScanViewControllerWithCsv:_appliedFormatsCsv];
59
+ }
60
+ return self;
61
+ }
62
+
63
+ #pragma mark - Scan view controller lifecycle
64
+
65
+ - (void)buildScanViewControllerWithCsv:(NSString *)csv
66
+ {
67
+ // (Re)create the VC. HmsCustomScanViewController takes its format set at init
68
+ // via HmsScanOptions and exposes no setter to change it later, so a formatsCsv
69
+ // change requires rebuilding the controller.
70
+ [self teardownScanViewController];
71
+
72
+ unsigned int formatType = [HmsScanResultMapper scanFormatTypeFromCsv:csv];
73
+ HmsScanOptions *options =
74
+ [[HmsScanOptions alloc] initWithScanFormatType:formatType Photo:NO];
75
+
76
+ HmsCustomScanViewController *vc =
77
+ [[HmsCustomScanViewController alloc] initCustomizedScanWithFormatType:options];
78
+ vc.backButtonHidden = YES; // we render our own chrome from JS
79
+ vc.customizedScanDelegate = self;
80
+ vc.continuouslyScan = _appliedContinuous;
81
+
82
+ _scanVC = vc;
83
+ _appliedFormatsCsv = csv ?: @"";
84
+
85
+ // Embed the VC's view as our content view, filling self.
86
+ UIView *scanView = vc.view;
87
+ scanView.frame = self.bounds;
88
+ scanView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
89
+ self.contentView = scanView;
90
+
91
+ // If we're already on screen, (re)attach to the nearest view controller.
92
+ if (self.window != nil) {
93
+ [self attachChildViewController];
94
+ }
95
+
96
+ // Re-apply paused state after a rebuild (continuouslyScan was set above).
97
+ if (_appliedPaused) {
98
+ [_scanVC pauseContinuouslyScan];
99
+ } else {
100
+ [_scanVC resumeContinuouslyScan];
101
+ }
102
+ }
103
+
104
+ - (void)teardownScanViewController
105
+ {
106
+ if (_scanVC == nil) {
107
+ return;
108
+ }
109
+ [self detachChildViewController];
110
+ _scanVC.customizedScanDelegate = nil;
111
+ if (self.contentView == _scanVC.view) {
112
+ self.contentView = nil;
113
+ }
114
+ _scanVC = nil;
115
+ }
116
+
117
+ - (void)attachChildViewController
118
+ {
119
+ if (_scanVC == nil || _childAttached) {
120
+ return;
121
+ }
122
+ UIViewController *parent = [self reactViewController];
123
+ if (parent == nil) {
124
+ return; // try again on next didMoveToWindow
125
+ }
126
+ [parent addChildViewController:_scanVC];
127
+ // The view is already in our hierarchy (set as contentView); just notify.
128
+ [_scanVC didMoveToParentViewController:parent];
129
+ _childAttached = YES;
130
+ }
131
+
132
+ - (void)detachChildViewController
133
+ {
134
+ if (_scanVC == nil || !_childAttached) {
135
+ return;
136
+ }
137
+ [_scanVC willMoveToParentViewController:nil];
138
+ [_scanVC removeFromParentViewController];
139
+ _childAttached = NO;
140
+ }
141
+
142
+ - (void)didMoveToWindow
143
+ {
144
+ [super didMoveToWindow];
145
+ if (self.window != nil) {
146
+ [self attachChildViewController];
147
+ } else {
148
+ [self detachChildViewController];
149
+ }
150
+ }
151
+
152
+ #pragma mark - Props
153
+
154
+ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &)oldProps
155
+ {
156
+ const auto &newViewProps = *std::static_pointer_cast<HmsScanViewProps const>(props);
157
+
158
+ NSString *newCsv = [NSString stringWithUTF8String:newViewProps.formatsCsv.c_str()];
159
+ if (newCsv == nil) {
160
+ newCsv = @"";
161
+ }
162
+ BOOL newContinuous = newViewProps.continuous;
163
+ BOOL newPaused = newViewProps.paused;
164
+ BOOL newTorch = newViewProps.torch;
165
+
166
+ BOOL first = !_hasAppliedOnce;
167
+
168
+ // formatsCsv change -> rebuild the controller (carry over continuous/paused).
169
+ if (first || ![newCsv isEqualToString:_appliedFormatsCsv]) {
170
+ _appliedContinuous = newContinuous;
171
+ _appliedPaused = newPaused;
172
+ [self buildScanViewControllerWithCsv:newCsv];
173
+ } else {
174
+ // continuous change -> toggle continuouslyScan in place.
175
+ if (newContinuous != _appliedContinuous) {
176
+ _appliedContinuous = newContinuous;
177
+ _scanVC.continuouslyScan = newContinuous;
178
+ }
179
+ // paused change -> pause/resume in place.
180
+ if (newPaused != _appliedPaused) {
181
+ _appliedPaused = newPaused;
182
+ if (newPaused) {
183
+ [_scanVC pauseContinuouslyScan];
184
+ } else {
185
+ [_scanVC resumeContinuouslyScan];
186
+ }
187
+ }
188
+ }
189
+
190
+ // torch change (best-effort, see applyTorch:).
191
+ if (first || newTorch != _appliedTorch) {
192
+ _appliedTorch = newTorch;
193
+ [self applyTorch:newTorch];
194
+ }
195
+
196
+ _hasAppliedOnce = YES;
197
+
198
+ [super updateProps:props oldProps:oldProps];
199
+ }
200
+
201
+ #pragma mark - Torch (best-effort)
202
+
203
+ // HUAWEI Scan Kit exposes NO public torch API on iOS — HmsCustomScanViewController
204
+ // renders its own torch button and auto-detects low light. We therefore drive the
205
+ // torch directly through AVFoundation as a best-effort convenience. This is NOT
206
+ // guaranteed to stay in sync with HUAWEI's built-in torch button, and may be a
207
+ // no-op if HUAWEI holds an exclusive lock on the capture device's configuration.
208
+ - (void)applyTorch:(BOOL)on
209
+ {
210
+ BOOL available = NO;
211
+ BOOL torchOn = [self setTorchHardwareOn:on available:&available];
212
+ [self emitTorchStatusAvailable:available on:torchOn];
213
+ }
214
+
215
+ // Mutates the capture device's torch. Returns the resulting on-state and writes
216
+ // hardware availability into `available`. Does NOT emit any event (so it can be
217
+ // reused on teardown/recycle without firing onTorchStatus on a stale emitter).
218
+ - (BOOL)setTorchHardwareOn:(BOOL)on available:(BOOL *)available
219
+ {
220
+ AVCaptureDevice *device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
221
+ BOOL isAvailable = (device != nil && device.hasTorch && device.isTorchAvailable);
222
+ if (available != NULL) {
223
+ *available = isAvailable;
224
+ }
225
+ if (!isAvailable) {
226
+ return NO;
227
+ }
228
+
229
+ BOOL torchOn = NO;
230
+ NSError *error = nil;
231
+ if ([device lockForConfiguration:&error]) {
232
+ if (on && [device isTorchModeSupported:AVCaptureTorchModeOn]) {
233
+ device.torchMode = AVCaptureTorchModeOn;
234
+ torchOn = YES;
235
+ } else {
236
+ device.torchMode = AVCaptureTorchModeOff;
237
+ torchOn = NO;
238
+ }
239
+ [device unlockForConfiguration];
240
+ }
241
+ // On lock failure we silently ignore (best-effort) and report torchOn = NO.
242
+ return torchOn;
243
+ }
244
+
245
+ #pragma mark - CustomizedScanDelegate
246
+
247
+ // Called by HUAWEI on every decode (repeatedly in continuous mode). `resultDic`
248
+ // is a single result dictionary; we wrap it into the contract's top-level array.
249
+ - (void)customizedScanDelegateForResult:(NSDictionary *)resultDic
250
+ {
251
+ NSDictionary *mapped = [HmsScanResultMapper scanResultFromHuaweiDict:resultDic];
252
+ if (mapped == nil) {
253
+ // Decoded payload had no usable value; surface as a soft error event.
254
+ [self emitScanErrorWithCode:@"E_NO_RESULT" message:@"扫码结果为空或无法解析"];
255
+ return;
256
+ }
257
+ NSString *json = [HmsScanResultMapper jsonStringFromScanResults:@[ mapped ]];
258
+ [self emitScanResultJson:json];
259
+ }
260
+
261
+ #pragma mark - Event emitters
262
+
263
+ - (void)emitScanResultJson:(NSString *)json
264
+ {
265
+ if (!_eventEmitter) {
266
+ return;
267
+ }
268
+ std::string resultsJson = json ? std::string([json UTF8String]) : std::string("[]");
269
+ std::static_pointer_cast<const HmsScanViewEventEmitter>(_eventEmitter)
270
+ ->onScanResult(HmsScanViewEventEmitter::OnScanResult{
271
+ .resultsJson = resultsJson,
272
+ });
273
+ }
274
+
275
+ - (void)emitScanErrorWithCode:(NSString *)code message:(NSString *)message
276
+ {
277
+ if (!_eventEmitter) {
278
+ return;
279
+ }
280
+ std::static_pointer_cast<const HmsScanViewEventEmitter>(_eventEmitter)
281
+ ->onScanError(HmsScanViewEventEmitter::OnScanError{
282
+ .code = code ? std::string([code UTF8String]) : std::string(""),
283
+ .message = message ? std::string([message UTF8String]) : std::string(""),
284
+ });
285
+ }
286
+
287
+ - (void)emitTorchStatusAvailable:(BOOL)available on:(BOOL)on
288
+ {
289
+ if (!_eventEmitter) {
290
+ return;
291
+ }
292
+ std::static_pointer_cast<const HmsScanViewEventEmitter>(_eventEmitter)
293
+ ->onTorchStatus(HmsScanViewEventEmitter::OnTorchStatus{
294
+ .available = available ? true : false,
295
+ .on = on ? true : false,
296
+ });
297
+ }
298
+
299
+ #pragma mark - Cleanup
300
+
301
+ - (void)prepareForRecycle
302
+ {
303
+ // Reset imperative state so a recycled view re-applies props cleanly.
304
+ // Turn the torch off WITHOUT emitting (the emitter is being torn down).
305
+ [self setTorchHardwareOn:NO available:NULL];
306
+ [self teardownScanViewController];
307
+ _hasAppliedOnce = NO;
308
+ _appliedFormatsCsv = @"";
309
+ _appliedContinuous = YES;
310
+ _appliedPaused = NO;
311
+ _appliedTorch = NO;
312
+ [super prepareForRecycle];
313
+ }
314
+
315
+ - (void)dealloc
316
+ {
317
+ if (_scanVC != nil) {
318
+ _scanVC.customizedScanDelegate = nil;
319
+ }
320
+ }
321
+
322
+ @end
@@ -0,0 +1,43 @@
1
+ "use strict";
2
+
3
+ import { forwardRef } from 'react';
4
+ import NativeHmsScanView from './HmsScanViewNativeComponent';
5
+ import { formatsToCsv, parseResultsJson } from "./format.js";
6
+
7
+ /** 暗光提示 / 手电状态。 */
8
+ import { jsx as _jsx } from "react/jsx-runtime";
9
+ /**
10
+ * 底层 headless 扫码相机组件:只渲染相机预览并发出扫码结果事件。
11
+ * 取景框 / 扫描线 / 手电按钮等 UI 由上层(如 `<Scanner>`)用普通 RN 视图叠加绘制。
12
+ *
13
+ * Android = 华为 RemoteView,iOS = HmsCustomScanViewController(Scan SDK-Plus / ScanKitFrameWork)。
14
+ */
15
+ export const HmsScanView = /*#__PURE__*/forwardRef(function HmsScanView({
16
+ formats,
17
+ continuous = true,
18
+ paused = false,
19
+ torch = false,
20
+ onScanResult,
21
+ onScanError,
22
+ onTorchStatus,
23
+ ...viewProps
24
+ }, ref) {
25
+ return /*#__PURE__*/_jsx(NativeHmsScanView, {
26
+ ...viewProps,
27
+ ref: ref,
28
+ formatsCsv: formatsToCsv(formats),
29
+ continuous: continuous,
30
+ paused: paused,
31
+ torch: torch,
32
+ onScanResult: onScanResult ? e => onScanResult(parseResultsJson(e.nativeEvent.resultsJson)) : undefined,
33
+ onScanError: onScanError ? e => onScanError({
34
+ code: e.nativeEvent.code,
35
+ message: e.nativeEvent.message
36
+ }) : undefined,
37
+ onTorchStatus: onTorchStatus ? e => onTorchStatus({
38
+ available: e.nativeEvent.available,
39
+ on: e.nativeEvent.on
40
+ }) : undefined
41
+ });
42
+ });
43
+ //# sourceMappingURL=HmsScanView.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["forwardRef","NativeHmsScanView","formatsToCsv","parseResultsJson","jsx","_jsx","HmsScanView","formats","continuous","paused","torch","onScanResult","onScanError","onTorchStatus","viewProps","ref","formatsCsv","e","nativeEvent","resultsJson","undefined","code","message","available","on"],"sourceRoot":"../../src","sources":["HmsScanView.tsx"],"mappings":";;AAAA,SAASA,UAAU,QAA2B,OAAO;AAErD,OAAOC,iBAAiB,MAAM,8BAA8B;AAC5D,SAASC,YAAY,EAAEC,gBAAgB,QAAQ,aAAU;;AAGzD;AAAA,SAAAC,GAAA,IAAAC,IAAA;AAyBA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,MAAMC,WAAW,gBAAGN,UAAU,CAGnC,SAASM,WAAWA,CACpB;EACEC,OAAO;EACPC,UAAU,GAAG,IAAI;EACjBC,MAAM,GAAG,KAAK;EACdC,KAAK,GAAG,KAAK;EACbC,YAAY;EACZC,WAAW;EACXC,aAAa;EACb,GAAGC;AACL,CAAC,EACDC,GAAG,EACH;EACA,oBACEV,IAAA,CAACJ,iBAAiB;IAAA,GACZa,SAAS;IACbC,GAAG,EAAEA,GAAI;IACTC,UAAU,EAAEd,YAAY,CAACK,OAAO,CAAE;IAClCC,UAAU,EAAEA,UAAW;IACvBC,MAAM,EAAEA,MAAO;IACfC,KAAK,EAAEA,KAAM;IACbC,YAAY,EACVA,YAAY,GACPM,CAAC,IAAKN,YAAY,CAACR,gBAAgB,CAACc,CAAC,CAACC,WAAW,CAACC,WAAW,CAAC,CAAC,GAChEC,SACL;IACDR,WAAW,EACTA,WAAW,GACNK,CAAC,IACAL,WAAW,CAAC;MACVS,IAAI,EAAEJ,CAAC,CAACC,WAAW,CAACG,IAAI;MACxBC,OAAO,EAAEL,CAAC,CAACC,WAAW,CAACI;IACzB,CAAC,CAAC,GACJF,SACL;IACDP,aAAa,EACXA,aAAa,GACRI,CAAC,IACAJ,aAAa,CAAC;MACZU,SAAS,EAAEN,CAAC,CAACC,WAAW,CAACK,SAAS;MAClCC,EAAE,EAAEP,CAAC,CAACC,WAAW,CAACM;IACpB,CAAC,CAAC,GACJJ;EACL,CACF,CAAC;AAEN,CAAC,CAAC","ignoreList":[]}