import ZenidSDK from "./internal/zenid-sdk.js"; import {zenidLog} from "./zenid-log.js"; import {CameraFacingMode, ICamera, IVerifier, IVisualizer, OnFrameProcessedCallback} from "../interfaces/verification-system-interfaces.js"; import { AcceptableInput, DocumentVerifierSettings, DocumentVerifierStateContainerForPublicData, EncodedImage, MsLivenessGiveMeResponse, MsLivenessStateContainerForPublicData, SdkWizardStep, SetupInfo, UploadReadyData, UploadSampleBasicInfo, VerifierSettingsBase, VerifierStateContainerForPublicDataBase } from "./generated/zenid-types.generated.js"; import { Country, DocumentCodes, DocumentRole, EncodedImageFormat, PageCodes, SampleType, SdkResponseType, SdkVerifierType, SupportedLanguages, VerifierState } from "./generated/zenid-enums.generated.js"; import {VerifierFactory} from "./verifiers/verifier-factory.js"; import {base64ToUint8Array} from "./helpers/binary-helpers.js"; import {BackendApi, SampleToUpload} from "../platforms/web/backend-api.js"; import {ZenidModel, zenidModels} from "./generated/zenid-models.generated.js"; import SdkWizardStepWithSettings, {WizardLogic} from "./wizard-logic.js"; import {CameraExplorer} from "../platforms/web/camera-explorer.js"; import {EmptyVisualizerImplementation} from "../platforms/web/web-visualizer.js"; class featureSettings { constructor( public feature: SdkVerifierType, public settings: VerifierSettingsBase ) { } } export class ZenidController { setupInfo: SetupInfo | null = null; wizard: WizardLogic = new WizardLogic(); camera!: ICamera; visualizer: IVisualizer = new EmptyVisualizerImplementation(); protected resourcePath!: string; protected useSingleThread: boolean = false; protected assetUrls?: { wasmUrl?: string; emscriptenJsUrl?: string }; protected challengeToken: string = ""; private verifier: IVerifier | null = null; private lastSettings: featureSettings | null = null; protected backendApi!: BackendApi; private snapshot: UploadReadyData | null = null; constructor() { } // PUBLIC METHODS async cameraPermissionDenied(): Promise { return !await CameraExplorer.isAnyCameraAvailable(); } canMakeSnapshots(): boolean { if (!this.verifier) return false; const verifiersEnablingSnapshots: SdkVerifierType[] = [SdkVerifierType.Selfie, SdkVerifierType.Document, SdkVerifierType.LicensePlate]; return verifiersEnablingSnapshots.includes(this.verifier.getVerifierType()); } async canSwitchCamera(): Promise { if (!this.verifier || !await CameraExplorer.canSwitchCamera(this.verifier.facingMode())) return false; const verifiersWithoutCameraControl: SdkVerifierType[] = [SdkVerifierType.MsLiveness]; return !verifiersWithoutCameraControl.includes(this.verifier.getVerifierType()); } createVerifierByType(verifierType: SdkVerifierType): IVerifier { return VerifierFactory.getVerifier(verifierType); } async demandAnotherPageOfDocumentIfNeeded(): Promise { let feature = this.verifier?.getVerifierType(); if (feature !== SdkVerifierType.Document) return; let status = await this.verifier!.getStateContainer() as DocumentVerifierStateContainerForPublicData; if (!status.DocumentCode || !status.PageCode || !status.SupportedPageCount) { return; } // Check if second page is needed using SupportedPageCount if (status.SupportedPageCount <= 1) return; // no second page needed let getDocumentStep = (page: PageCodes) => { let newSettings = Object.assign(new DocumentVerifierSettings(), this.lastSettings?.settings) if (newSettings) newSettings.AcceptableInput = { PossibleDocuments: [{ DocumentCode: status.DocumentCode, Page: page }]}; let step = new SdkWizardStep(); step.VerifierType = SdkVerifierType.Document; return new SdkWizardStepWithSettings(step, newSettings); } this.wizard?.demandSecondPageOfDocument(getDocumentStep(status.PageCode), getDocumentStep(status.PageCode === PageCodes.F ? PageCodes.B : PageCodes.F)); } async enableDebugVisualization(isEnabled: boolean): Promise { await ZenidSDK.Instance.setDebugVisualization(isEnabled); } getAllowedCountries(): Country[] { const countries = this.getAllowedModels() .map(model => model.country) .filter((value, index, self) => self.indexOf(value) === index); //this is .distinct() // Sort with CZ first, SK second, then alphabetically const priority: Record = { [Country.Cz]: 0, [Country.Sk]: 1 }; return countries.sort((a, b) => (priority[a] ?? 999) - (priority[b] ?? 999) || a.localeCompare(b)); } getAllowedDocuments(): DocumentCodes[] { return this.setupInfo!.AllowedDocumentCodes ?? []; } getAllowedRoles(country: Country | null): DocumentRole[] { return this.getAllowedModels() .filter(model => country === null || model.country === country) .map(model => model.documentRole) .filter((value, index, self) => self.indexOf(value) === index); //this is .distinct(); } getAcceptableModels(acceptableInput: AcceptableInput | undefined): ZenidModel[] | undefined { if (!acceptableInput || !acceptableInput.PossibleDocuments || acceptableInput.PossibleDocuments.length === 0) { return undefined; } return this.getAllowedModels() .filter(model => acceptableInput.PossibleDocuments?.some(doc => (!doc.DocumentCode || doc.DocumentCode === model.documentCode) && (!doc.Page || doc.Page === model.pageCode) && (!doc.Country || doc.Country === model.country) && (!doc.Role || doc.Role === model.documentRole))); } async getSample(sampleId: string): Promise { try { return await this.backendApi.getSample(sampleId); } catch (error) { zenidLog.error('Error fetching sample:', error); throw error; } } getSupportedPageCount(documentCode: DocumentCodes): Promise { return ZenidSDK.Instance.getSupportedPageCount(documentCode); } async init(backendApi: BackendApi, absoluteModelsUrl: string, useSingleThread: boolean = false): Promise { this.backendApi = backendApi; this.resourcePath = absoluteModelsUrl; this.useSingleThread = useSingleThread; if (!useSingleThread) this.checkCrossOriginIsolation(); // Start SDK setup and camera initialization in parallel. // assetUrls (optional CDN override for wasm/glue) is read from this.assetUrls — derived classes set it before calling super.init(). const sdkSetupPromise = ZenidSDK.Instance.setup(this.resourcePath, useSingleThread, this.assetUrls); const cameraInitPromise = CameraExplorer.init(); // Wait for SDK setup to complete before setting visualizer version await sdkSetupPromise; // Set global visualizer version await ZenidSDK.Instance.setVisualizerVersion(2); // Wait for all parallel operations to complete await this.initSdk(); } async investigate(sampleIds: string[]): Promise { return await this.backendApi.investigateSamples(sampleIds, false); } /** * Helper function to detect mobile devices (iOS and Android) * @returns {boolean} True if running on a mobile device, false otherwise */ isMobileDevice(): boolean { return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini|Mobile/i.test(navigator.userAgent); } /** * Helper function to detect iOS devices * @returns {boolean} True if running on iOS, false otherwise */ isIos(): boolean { return /iPad|iPhone|iPod/.test(navigator.userAgent) && !(window as any).MSStream; } async loadAndRunVerifier(verifierType: SdkVerifierType, settings: string | VerifierSettingsBase = {}, onFrameProcessed?: OnFrameProcessedCallback): Promise { try { await this.loadVerifier(verifierType, settings); return await this.runVerifier(onFrameProcessed); } catch (error) { zenidLog.error('Error loading and running verifier:', error); } return null; } async loadVerifier(verifierType: SdkVerifierType, settings: string | VerifierSettingsBase = {}): Promise { try { // Stop current verifier if (this.verifier !== null) { await this.verifier.stopProcessingLoop(); } // Create new example with selected verifier type this.verifier = this.createVerifierByType(verifierType); let settingsObj: VerifierSettingsBase = (typeof settings === "string") ? JSON.parse(settings) : settings; this.lastSettings = new featureSettings(verifierType, settingsObj); await this.verifier.initOrRestart(settings); return this.verifier; } catch (error) { zenidLog.error('Error initializing verifier:', error); return null; } } async onGiveMe(state: VerifierStateContainerForPublicDataBase): Promise { // Handle torch commands first (before switch) if (state.GiveMeWhat === SdkResponseType.TorchOn) { await this.camera?.setTorch(true); return "{}"; } if (state.GiveMeWhat === SdkResponseType.TorchOff) { await this.camera?.setTorch(false); return "{}"; } switch (state.GiveMeWhat) { case SdkResponseType.MsLiveness: return JSON.stringify(await this.startMsLiveness(state)); case SdkResponseType.Photo: { let documentState = state as DocumentVerifierStateContainerForPublicData; let photo = await this.takePhotoWithStream(this.verifier!.facingMode(), documentState.GiveMePhotoResolution); if (!photo || photo.Data == "") { zenidLog.error('No photo taken'); return null; } zenidLog.info(`Photo captured, data size: ${photo.Data?.length} bytes`); let encodedImage: EncodedImage = { Data: photo.Data, Format: EncodedImageFormat.JPEG, }; return JSON.stringify(encodedImage); } } return null; } async restartVerifierWithPreviousSettings(): Promise { if (!this.verifier) { zenidLog.error("No verifier to restart"); return; } await this.verifier.restartWithPreviousSettings(); } async runVerifier(onFrameProcessed?: OnFrameProcessedCallback): Promise { try { if (!this.verifier) { zenidLog.error('Verifier not initialized'); return null; } if (!this.lastSettings) { zenidLog.error('You have to load verifier first'); return null; } this.snapshot = null; await this.setupVisualElements(); let verifierResult = await this.verifier!.processFramesFromCamera(this.camera, this.visualizer, (state) => this.onGiveMe(state), onFrameProcessed); if (!verifierResult && this.snapshot) return this.snapshot; return verifierResult; } catch (error) { // Central sink for verification-run errors: everything thrown by the verifier loop, // onGiveMe, or the camera converges here. The logger renders Error message + stack; // non-Error throws are stringified, so the cause is always recorded. zenidLog.error('Verification run aborted by an unhandled error:', error); } return null; } async selectProfile(value: string) { await ZenidSDK.Instance.selectProfile(value); await this.initRequiredSteps(value); } async setLanguage(language: SupportedLanguages): Promise { await ZenidSDK.Instance.setLanguage(language); } async setSetupInfo(): Promise { this.setupInfo = await this.backendApi.getSetupInfo(); const disabledVerifiers: SdkVerifierType[] = [ SdkVerifierType.IQSHologram ]; this.setupInfo.AllowedSdkVerifiers = this.setupInfo.AllowedSdkVerifiers?.filter(v => !disabledVerifiers.includes(v)); await this.initRequiredSteps(this.setupInfo?.Profiles?.[0]); } async stop(): Promise { await this.camera?.stop(); await this.verifier?.stopProcessingLoop(); } async switchCamera(): Promise { await this.camera.switchCamera(); } async takeImmediateSnapshot(): Promise { let verifierType = this.verifier?.getVerifierType(); if (!verifierType) { throw new Error('Verifier is not initialized'); } if (!this.canMakeSnapshots()) { throw new Error('Immediate snapshot is not supported for this verifier type'); } let photo: EncodedImage = await this.takeNativePhoto(this.verifier!.facingMode()); if (!photo?.Data || photo.Data == "") { throw new Error('No camera frame available'); } this.snapshot = { ImagePreview: photo.Data, SignedSamplePackage: photo.Data, SampleType: this.getExpectedSampleType()!, }; // then stop await this.stop(); } async takeNativePhoto(facingMode: CameraFacingMode): Promise { throw new Error('Not implemented'); } async takePhotoWithStream(facingMode: CameraFacingMode, resolution?: number): Promise { throw new Error('Not implemented'); } async uploadSample(): Promise { if (this.snapshot) return await this.onSuccess(); const verifierOutput = await this.verifier!.getStateContainer() as VerifierStateContainerForPublicDataBase; if (verifierOutput === null) { zenidLog.warn('Not expected: no output from verifier'); } else { await this.camera.stop(); switch (verifierOutput?.State) { case VerifierState.Failed: zenidLog.info("Verification failed."); break; case VerifierState.InProgress: zenidLog.info("Verification aborted."); break; case VerifierState.Success: return await this.onSuccess(); default: zenidLog.info("Verification completed"); } } return null; } async verifyAndUpload(verifierType: SdkVerifierType, settings: string | VerifierSettingsBase = "", onFrameProcessed?: OnFrameProcessedCallback): Promise { await this.loadAndRunVerifier(verifierType, settings, onFrameProcessed); return await this.uploadSample(); } // PROTECTED METHODS protected async initSdk(): Promise { let estimatedMemoryUsage: any = await ZenidSDK.Instance.measureAvailableRAM(); // Parse the result as a tuple of [available RAM, total RAM] let obj = JSON.parse(estimatedMemoryUsage); zenidLog.info('contiguousMB:', obj.contiguousMB); zenidLog.info('fragmentedMB:', obj.fragmentedMB); if (obj.fragmentedMB < 128 || obj.contiguousMB < 120) zenidLog.warn('Estimated memory usage is very low, some functionality will be unavailable.'); this.challengeToken = await ZenidSDK.Instance.getChallengeToken(); zenidLog.info("Challenge token: ", this.challengeToken); // request initSdk const responseToken = await this.backendApi.initSdk(this.challengeToken); const result = await ZenidSDK.Instance.authorize(responseToken); if (!result) { zenidLog.error('Authorization failed'); return; } } protected async setupVisualElements(): Promise { if (!this.camera || !this.visualizer) { throw new Error('Camera or visualizer not set'); } await this.camera.init(); await this.visualizer.init(); } protected async startMsLiveness(state: MsLivenessStateContainerForPublicData): Promise { throw new Error("Not implemented!"); } // PRIVATE METHODS private checkCrossOriginIsolation(): void { if (!self.crossOriginIsolated) { const errorMessage = 'Cross-origin isolation is not enabled. Please enable it to use the SDK.'; zenidLog.error(errorMessage); throw new Error(errorMessage); } } private getAllowedModels(): ZenidModel[] { return zenidModels .filter(model => this.setupInfo!.AllowedDocumentCodes?.some(dc => model.documentCode === dc)); } private getExpectedSampleType(): SampleType | null { switch (this.verifier?.getVerifierType()) { case SdkVerifierType.Document: case SdkVerifierType.Hologram: case SdkVerifierType.IQSHologram: return SampleType.DocumentPicture; case SdkVerifierType.FaceLiveness: case SdkVerifierType.Selfie: return SampleType.Selfie; case SdkVerifierType.LicensePlate: return SampleType.LicensePlate; default: return null; } } private async initRequiredSteps(selectedProfile?: string) { let steps: SdkWizardStep[] = []; if (!selectedProfile || !this.setupInfo || !this.setupInfo.RequiredSteps) { steps = await ZenidSDK.Instance.getSdkWizardSteps(); } else { steps = this.setupInfo?.RequiredSteps?.[selectedProfile] ?? []; } this.wizard?.setRequiredSteps(...steps); } private async onSuccess(): Promise { let data: UploadReadyData = this.snapshot ?? await this.verifier!.getUploadReadyData(); let sampleToUpload = new SampleToUpload(base64ToUint8Array(data.SignedSamplePackage!)); if (this.snapshot) { let documentState = await this.verifier?.getStateContainer() as DocumentVerifierStateContainerForPublicData; if (documentState && documentState.DocumentCode && documentState.PageCode) { sampleToUpload.set(documentState.DocumentCode); sampleToUpload.set(documentState.PageCode); } else { const acceptableInput = (this.verifier?.getSettings() as DocumentVerifierSettings)?.AcceptableInput; if (acceptableInput) { sampleToUpload.set(Object.assign(new AcceptableInput(), acceptableInput)); } } } try { const response = await this.backendApi.uploadSample(sampleToUpload); if (!response || response.ErrorCode) { throw new Error(`apiError: ${response.ErrorCode} ${response.ErrorText}`); } return response; } catch (error) { zenidLog.error('Error uploading file:', error); throw error; } } }