-
Notifications
You must be signed in to change notification settings - Fork 15
feat: Auto scanning of receipts #3850
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 1 commit
b91f06b
49fe16c
cdb19e9
5c4066d
f13fd7c
4f057dd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,39 @@ | ||||||||||||||
| import { Injectable } from '@angular/core'; | ||||||||||||||
| import { DocumentScanner, ScanResult } from '@capacitor-mlkit/document-scanner'; | ||||||||||||||
| import { from, Observable } from 'rxjs'; | ||||||||||||||
| import { switchMap } from 'rxjs/operators'; | ||||||||||||||
|
|
||||||||||||||
| @Injectable({ | ||||||||||||||
| providedIn: 'root', | ||||||||||||||
| }) | ||||||||||||||
| export class MLKitDocumentScannerService { | ||||||||||||||
| scanDocument(): Observable<string> { | ||||||||||||||
| return from(DocumentScanner.scanDocument({ resultFormats: 'JPEG', galleryImportAllowed: true, pageLimit: 1 })).pipe( | ||||||||||||||
| switchMap((result: ScanResult) => { | ||||||||||||||
| if (result.scannedImages && result.scannedImages.length > 0) { | ||||||||||||||
| // MLKit returns file URLs, so we need to convert them to base64 | ||||||||||||||
| return from(this.convertFileToBase64(result.scannedImages[0])); | ||||||||||||||
| } | ||||||||||||||
| throw new Error('No document scanned'); | ||||||||||||||
|
||||||||||||||
| }), | ||||||||||||||
| ); | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| private async convertFileToBase64(fileUrl: string): Promise<string> { | ||||||||||||||
| try { | ||||||||||||||
| const response = await fetch(fileUrl); | ||||||||||||||
| const blob = await response.blob(); | ||||||||||||||
| return new Promise<string>((resolve, reject): void => { | ||||||||||||||
| const reader = new FileReader(); | ||||||||||||||
| reader.onloadend = (): void => { | ||||||||||||||
| const base64data = reader.result as string; | ||||||||||||||
| resolve(base64data); | ||||||||||||||
| }; | ||||||||||||||
| reader.onerror = (): void => reject(new Error('Failed to convert file to base64')); | ||||||||||||||
| reader.readAsDataURL(blob); | ||||||||||||||
| }); | ||||||||||||||
| } catch { | ||||||||||||||
| throw new Error('Failed to convert file to base64'); | ||||||||||||||
| } | ||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Mind it! The error details are vanishing like a villain in my movies! The generic catch block swallows the original error information, making debugging difficult. Preserve the error details for better troubleshooting. - } catch {
- throw new Error('Failed to convert file to base64');
+ } catch (error) {
+ throw new Error(`Failed to convert file to base64: ${error.message || 'Unknown error'}`);📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||
| } | ||||||||||||||
| } | ||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,4 +1,5 @@ | ||||||||||||||||||||
| import { Component, EventEmitter, OnDestroy, OnInit, Input, AfterViewInit, ViewChild, Inject } from '@angular/core'; | ||||||||||||||||||||
| import { Component, EventEmitter, OnDestroy, OnInit, Input, AfterViewInit, ViewChild } from '@angular/core'; | ||||||||||||||||||||
| import { inject } from '@angular/core'; | ||||||||||||||||||||
| import { CameraPreviewPictureOptions } from '@capacitor-community/camera-preview'; | ||||||||||||||||||||
| import { ModalController, NavController, PopoverController } from '@ionic/angular'; | ||||||||||||||||||||
| import { ReceiptPreviewComponent } from './receipt-preview/receipt-preview.component'; | ||||||||||||||||||||
|
|
@@ -23,6 +24,7 @@ import { SnackbarPropertiesService } from 'src/app/core/services/snackbar-proper | |||||||||||||||||||
| import { AuthService } from 'src/app/core/services/auth.service'; | ||||||||||||||||||||
| import { CameraService } from 'src/app/core/services/camera.service'; | ||||||||||||||||||||
| import { CameraPreviewService } from 'src/app/core/services/camera-preview.service'; | ||||||||||||||||||||
| import { MLKitDocumentScannerService } from 'src/app/core/services/mlkit-document-scanner.service'; | ||||||||||||||||||||
| import { ReceiptPreviewData } from 'src/app/core/models/receipt-preview-data.model'; | ||||||||||||||||||||
| import { TranslocoService } from '@jsverse/transloco'; | ||||||||||||||||||||
|
|
||||||||||||||||||||
|
|
@@ -65,26 +67,43 @@ export class CaptureReceiptComponent implements OnInit, OnDestroy, AfterViewInit | |||||||||||||||||||
|
|
||||||||||||||||||||
| nativeSettings = NativeSettings; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| constructor( | ||||||||||||||||||||
| private modalController: ModalController, | ||||||||||||||||||||
| private trackingService: TrackingService, | ||||||||||||||||||||
| private router: Router, | ||||||||||||||||||||
| private navController: NavController, | ||||||||||||||||||||
| private transactionsOutboxService: TransactionsOutboxService, | ||||||||||||||||||||
| private imagePicker: ImagePicker, | ||||||||||||||||||||
| private networkService: NetworkService, | ||||||||||||||||||||
| private popoverController: PopoverController, | ||||||||||||||||||||
| private loaderService: LoaderService, | ||||||||||||||||||||
| private orgService: OrgService, | ||||||||||||||||||||
| private platformEmployeeSettingsService: PlatformEmployeeSettingsService, | ||||||||||||||||||||
| private matSnackBar: MatSnackBar, | ||||||||||||||||||||
| private snackbarProperties: SnackbarPropertiesService, | ||||||||||||||||||||
| private authService: AuthService, | ||||||||||||||||||||
| private cameraService: CameraService, | ||||||||||||||||||||
| private cameraPreviewService: CameraPreviewService, | ||||||||||||||||||||
| @Inject(DEVICE_PLATFORM) private devicePlatform: 'android' | 'ios' | 'web', | ||||||||||||||||||||
| private translocoService: TranslocoService, | ||||||||||||||||||||
| ) {} | ||||||||||||||||||||
| private readonly modalController = inject(ModalController); | ||||||||||||||||||||
|
|
||||||||||||||||||||
| private readonly trackingService = inject(TrackingService); | ||||||||||||||||||||
|
|
||||||||||||||||||||
| private readonly router = inject(Router); | ||||||||||||||||||||
|
|
||||||||||||||||||||
| private readonly navController = inject(NavController); | ||||||||||||||||||||
|
|
||||||||||||||||||||
| private readonly transactionsOutboxService = inject(TransactionsOutboxService); | ||||||||||||||||||||
|
|
||||||||||||||||||||
| private readonly imagePicker = inject(ImagePicker); | ||||||||||||||||||||
|
|
||||||||||||||||||||
| private readonly networkService = inject(NetworkService); | ||||||||||||||||||||
|
|
||||||||||||||||||||
| private readonly popoverController = inject(PopoverController); | ||||||||||||||||||||
|
|
||||||||||||||||||||
| private readonly loaderService = inject(LoaderService); | ||||||||||||||||||||
|
|
||||||||||||||||||||
| private readonly orgService = inject(OrgService); | ||||||||||||||||||||
|
|
||||||||||||||||||||
| private readonly platformEmployeeSettingsService = inject(PlatformEmployeeSettingsService); | ||||||||||||||||||||
|
|
||||||||||||||||||||
| private readonly matSnackBar = inject(MatSnackBar); | ||||||||||||||||||||
|
|
||||||||||||||||||||
| private readonly snackbarProperties = inject(SnackbarPropertiesService); | ||||||||||||||||||||
|
|
||||||||||||||||||||
| private readonly authService = inject(AuthService); | ||||||||||||||||||||
|
|
||||||||||||||||||||
| private readonly cameraService = inject(CameraService); | ||||||||||||||||||||
|
|
||||||||||||||||||||
| private readonly cameraPreviewService = inject(CameraPreviewService); | ||||||||||||||||||||
|
|
||||||||||||||||||||
| private readonly mlkitScanner = inject(MLKitDocumentScannerService); | ||||||||||||||||||||
|
|
||||||||||||||||||||
| private readonly devicePlatform = inject<'android' | 'ios' | 'web'>(DEVICE_PLATFORM); | ||||||||||||||||||||
|
|
||||||||||||||||||||
| private readonly translocoService = inject(TranslocoService); | ||||||||||||||||||||
|
|
||||||||||||||||||||
| setupNetworkWatcher(): void { | ||||||||||||||||||||
| const networkWatcherEmitter = new EventEmitter<boolean>(); | ||||||||||||||||||||
|
|
@@ -382,27 +401,32 @@ export class CaptureReceiptComponent implements OnInit, OnDestroy, AfterViewInit | |||||||||||||||||||
| this.trackingService.receiptLimitReached(); | ||||||||||||||||||||
| this.showLimitReachedPopover().subscribe(noop); | ||||||||||||||||||||
| } else { | ||||||||||||||||||||
| const cameraPreviewPictureOptions: CameraPreviewPictureOptions = { | ||||||||||||||||||||
| quality: 70, | ||||||||||||||||||||
| }; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| from(this.cameraPreviewService.capture(cameraPreviewPictureOptions)).subscribe((receiptData) => { | ||||||||||||||||||||
| const base64PictureData = 'data:image/jpeg;base64,' + receiptData.value; | ||||||||||||||||||||
| this.lastCapturedReceipt = base64PictureData; | ||||||||||||||||||||
| if (!this.isBulkMode) { | ||||||||||||||||||||
| this.stopCamera(); | ||||||||||||||||||||
| this.base64ImagesWithSource.push({ | ||||||||||||||||||||
| source: 'MOBILE_DASHCAM_SINGLE', | ||||||||||||||||||||
| base64Image: base64PictureData, | ||||||||||||||||||||
| }); | ||||||||||||||||||||
| this.onSingleCapture(); | ||||||||||||||||||||
| } else { | ||||||||||||||||||||
| this.base64ImagesWithSource.push({ | ||||||||||||||||||||
| source: 'MOBILE_DASHCAM_BULK', | ||||||||||||||||||||
| base64Image: base64PictureData, | ||||||||||||||||||||
| this.mlkitScanner.scanDocument().subscribe({ | ||||||||||||||||||||
| next: (base64PictureData) => { | ||||||||||||||||||||
| this.lastCapturedReceipt = base64PictureData; | ||||||||||||||||||||
| if (!this.isBulkMode) { | ||||||||||||||||||||
| this.stopCamera(); | ||||||||||||||||||||
| this.base64ImagesWithSource.push({ | ||||||||||||||||||||
| source: 'MOBILE_DASHCAM_SINGLE', | ||||||||||||||||||||
| base64Image: base64PictureData, | ||||||||||||||||||||
| }); | ||||||||||||||||||||
| this.onSingleCapture(); | ||||||||||||||||||||
| } else { | ||||||||||||||||||||
| this.base64ImagesWithSource.push({ | ||||||||||||||||||||
| source: 'MOBILE_DASHCAM_BULK', | ||||||||||||||||||||
| base64Image: base64PictureData, | ||||||||||||||||||||
| }); | ||||||||||||||||||||
| this.onBulkCapture(); | ||||||||||||||||||||
| } | ||||||||||||||||||||
| }, | ||||||||||||||||||||
| // eslint-disable-next-line @typescript-eslint/no-unused-vars | ||||||||||||||||||||
| error: (error) => { | ||||||||||||||||||||
| this.matSnackBar.openFromComponent(ToastMessageComponent, { | ||||||||||||||||||||
| ...this.snackbarProperties.setSnackbarProperties('failure', { | ||||||||||||||||||||
| message: 'Camera not available', | ||||||||||||||||||||
| }), | ||||||||||||||||||||
| }); | ||||||||||||||||||||
| this.onBulkCapture(); | ||||||||||||||||||||
| } | ||||||||||||||||||||
| }, | ||||||||||||||||||||
|
||||||||||||||||||||
| }); | ||||||||||||||||||||
|
||||||||||||||||||||
| } | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
@@ -515,14 +539,16 @@ export class CaptureReceiptComponent implements OnInit, OnDestroy, AfterViewInit | |||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| setUpAndStartCamera(): void { | ||||||||||||||||||||
| this.cameraPreview.setUpAndStartCamera(); | ||||||||||||||||||||
| // MLKit scanner will handle camera setup and permissions | ||||||||||||||||||||
| this.onCaptureReceipt(); | ||||||||||||||||||||
|
|
||||||||||||||||||||
| if (this.transactionsOutboxService.singleCaptureCount === 3) { | ||||||||||||||||||||
| this.showBulkModeToastMessage(); | ||||||||||||||||||||
| this.isBulkModePromptShown = true; | ||||||||||||||||||||
| } | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| stopCamera(): void { | ||||||||||||||||||||
| this.cameraPreview.stopCamera(); | ||||||||||||||||||||
| // MLKit scanner handles its own camera lifecycle | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick (assertive) Mind it! Empty methods need explanation, like my punch dialogues need context! While MLKit handles camera lifecycle, consider documenting why this method is kept empty or remove it entirely if no longer needed. stopCamera(): void {
- // MLKit scanner handles its own camera lifecycle
+ // MLKit scanner handles its own camera lifecycle
+ // Method retained for compatibility with existing call sites
+ // Can be removed after refactoring all references
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||
| } | ||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
Mind it! These new packages need my blessing for security!
The new dependencies look stylish, but let me verify they're the latest and safest versions for my fans to use.
Also applies to: 54-54
🌐 Web query:
💡 Result:
As of August 7, 2025, the latest versions of the specified npm packages are:
@capacitor-mlkit/document-scanner: The most recent version is
7.2.1, released on April 7, 2025. (classic.yarnpkg.com)@capacitor/filesystem: The latest stable version is
7.0.1, published 18 days ago. (npmjs.com)Please note that
@capacitor/filesystemhas a nightly build version7.0.2-nightly-20250527T150614.0, released 19 days ago. (npmjs.com)For the most current information, you can visit the npm package pages:
@capacitor-mlkit/document-scanner: (classic.yarnpkg.com)
@capacitor/filesystem: (npmjs.com)
Citations:
I will verify the version of @capacitor/filesystem in package.json now, mighty developer!
🏁 Script executed:
Length of output: 84
Double-check published versions of new dependencies, Superstar!
I see two package versions that leap beyond the latest npm releases:
• Latest published is 7.2.1 (yarnpkg.com)
• Latest stable is 7.0.1 (npmjs.com)
Please confirm these versions actually exist on npm or adjust them down to the current stable releases. Rajini says: get this right at once!
🤖 Prompt for AI Agents