From 2f8a49692a3516fe8307be2c852ab302bc3fff8f Mon Sep 17 00:00:00 2001 From: Sam David Date: Wed, 3 Apr 2024 22:49:07 +0530 Subject: [PATCH] show error & disable dynamic scan if device not available --- app/components/ak-select/index.ts | 2 +- app/components/dynamic-scan/modal/index.hbs | 379 +++++++++--------- .../device-preference/index.hbs | 66 +++ .../device-preference/index.ts | 24 ++ app/components/project-preferences/index.hbs | 63 +-- app/components/project-preferences/index.ts | 157 +------- .../project-preferences/provider/index.hbs | 11 + .../project-preferences/provider/index.ts | 214 ++++++++++ .../general-settings/index.hbs | 13 +- mirage/factories/project-available-device.ts | 2 +- .../components/dynamic-scan-test.js | 103 ++++- translations/en.json | 1 + translations/ja.json | 1 + 13 files changed, 647 insertions(+), 389 deletions(-) create mode 100644 app/components/project-preferences/device-preference/index.hbs create mode 100644 app/components/project-preferences/device-preference/index.ts create mode 100644 app/components/project-preferences/provider/index.hbs create mode 100644 app/components/project-preferences/provider/index.ts diff --git a/app/components/ak-select/index.ts b/app/components/ak-select/index.ts index 2e0de3612d..af636708e6 100644 --- a/app/components/ak-select/index.ts +++ b/app/components/ak-select/index.ts @@ -20,7 +20,7 @@ interface AkSelectNamedArgs extends PowerSelectArgs { dropdownClass?: string; placeholder?: string; renderInPlace?: boolean; - error?: string; + error?: boolean; loadingMessage?: string; selectedItemComponent?: string; labelTypographyVariant?: AkSelectLabelTypographyVariant; diff --git a/app/components/dynamic-scan/modal/index.hbs b/app/components/dynamic-scan/modal/index.hbs index f51c9a4b7e..c2654d57ab 100644 --- a/app/components/dynamic-scan/modal/index.hbs +++ b/app/components/dynamic-scan/modal/index.hbs @@ -1,207 +1,216 @@ - - <:default> -
- - - - - {{t 'modalCard.dynamicScan.warning'}} - - + + <:default> +
+ + - {{#if @file.minOsVersion}} -
- - {{t 'modalCard.dynamicScan.deviceRequirements'}} + + {{t 'modalCard.dynamicScan.warning'}} + - - {{#each this.deviceRequirements as |dr|}} - - - {{dr.type}} - + {{#if @file.minOsVersion}} +
+ + {{t 'modalCard.dynamicScan.deviceRequirements'}} + - + {{#each this.deviceRequirements as |dr|}} + - - {{dr.boldValue}} - + + {{dr.type}} + + + + + {{dr.boldValue}} + + + {{dr.value}} + + + {{/each}} + +
+ {{/if}} - {{dr.value}} -
- - {{/each}} - +
+
- {{/if}} - -
- -
- - {{#unless @file.showScheduleAutomatedDynamicScan}} - - {{t 'note'}}: - {{t 'modalCard.dynamicScan.deviceSettingsWarning'}} - - {{/unless}} - -
- -
-
- - - - - - - {{#if this.showApiScanSettings}} -
- - {{t 'modalCard.dynamicScan.apiScanDescription' htmlSafe=true}} - - - + {{t 'note'}}: + {{t 'modalCard.dynamicScan.deviceSettingsWarning'}} + + {{/unless}} - -
- {{/if}} +
+ +
- {{#if @file.showScheduleAutomatedDynamicScan}} -
+ - + + + + + + {{#if this.showApiScanSettings}} +
- {{t 'dynamicScanAutomation'}} + {{t 'modalCard.dynamicScan.apiScanDescription' htmlSafe=true}} - - <:icon> - - - - + - +
+ {{/if}} + + {{#if @file.showScheduleAutomatedDynamicScan}} +
- {{t 'scheduleDynamicscanDesc'}} - + + + {{t 'dynamicScanAutomation'}} + + + + <:icon> + + + + - - - <:leftIcon> - {{#if this.scheduleDynamicScan.isRunning}} - - {{else}} - - {{/if}} - - - <:default> - {{t 'scheduleDynamicscan'}} - - - -
- {{/if}} + {{t 'scheduleDynamicscanDesc'}} + + + + + <:leftIcon> + {{#if this.scheduleDynamicScan.isRunning}} + + {{else}} + + {{/if}} + + + <:default> + {{t 'scheduleDynamicscan'}} + + + +
+ {{/if}} +
-
- - - <:footer> - - - - - {{t 'cancel'}} - - - - {{t 'modalCard.dynamicScan.start'}} - - - -
\ No newline at end of file + + + <:footer> + + + + + {{t 'cancel'}} + + + + {{t 'modalCard.dynamicScan.start'}} + + + + + \ No newline at end of file diff --git a/app/components/project-preferences/device-preference/index.hbs b/app/components/project-preferences/device-preference/index.hbs new file mode 100644 index 0000000000..a147d9442f --- /dev/null +++ b/app/components/project-preferences/device-preference/index.hbs @@ -0,0 +1,66 @@ + + {{#if (has-block 'title')}} + {{yield to='title'}} + {{else}} + + {{t 'devicePreferences'}} + + {{/if}} + + + {{t 'otherTemplates.selectPreferredDevice'}} + + + + +
+ + {{t (device-type aks.value)}} + +
+ +
+ + {{#if (eq version '0')}} + {{t 'anyVersion'}} + {{else}} + {{version}} + {{/if}} + +
+
+ +{{#if this.isPreferredDeviceNotAvailable}} + + + + + {{t 'modalCard.dynamicScan.preferredDeviceNotAvailable'}} + + +{{/if}} \ No newline at end of file diff --git a/app/components/project-preferences/device-preference/index.ts b/app/components/project-preferences/device-preference/index.ts new file mode 100644 index 0000000000..093651bdaf --- /dev/null +++ b/app/components/project-preferences/device-preference/index.ts @@ -0,0 +1,24 @@ +import Component from '@glimmer/component'; +import { DevicePreferenceContext } from '../provider'; + +export interface ProjectPreferencesDevicePreferenceSignature { + Args: { + dpContext: DevicePreferenceContext; + }; + Blocks: { + title: []; + }; +} + +export default class ProjectPreferencesDevicePreferenceComponent extends Component { + get isPreferredDeviceNotAvailable() { + return this.args.dpContext.isPreferredDeviceAvailable === false; + } +} + +declare module '@glint/environment-ember-loose/registry' { + export default interface Registry { + 'ProjectPreferences::DevicePreference': typeof ProjectPreferencesDevicePreferenceComponent; + 'project-preferences/device-preference': typeof ProjectPreferencesDevicePreferenceComponent; + } +} diff --git a/app/components/project-preferences/index.hbs b/app/components/project-preferences/index.hbs index 8bb31fb2e8..3e8656e946 100644 --- a/app/components/project-preferences/index.hbs +++ b/app/components/project-preferences/index.hbs @@ -1,49 +1,14 @@ - - {{#if (has-block 'title')}} - {{yield to='title'}} - {{else}} - - {{t 'devicePreferences'}} - - {{/if}} - - - {{t 'otherTemplates.selectPreferredDevice'}} - - - - -
- - {{t (device-type aks.value)}} - -
- -
- - {{#if (eq version '0')}} - {{t 'anyVersion'}} - {{else}} - {{version}} - {{/if}} - -
-
\ No newline at end of file + + {{yield + (hash + DevicePreferenceComponent=(component + 'project-preferences/device-preference' dpContext=dpContext + ) + ) + }} + \ No newline at end of file diff --git a/app/components/project-preferences/index.ts b/app/components/project-preferences/index.ts index e6d6208056..f56830e4b9 100644 --- a/app/components/project-preferences/index.ts +++ b/app/components/project-preferences/index.ts @@ -1,19 +1,8 @@ -// eslint-disable-next-line ember/use-ember-data-rfc-395-imports -import DS from 'ember-data'; - import Component from '@glimmer/component'; -import { inject as service } from '@ember/service'; -import ENUMS from 'irene/enums'; -import ENV from 'irene/config/environment'; -import IntlService from 'ember-intl/services/intl'; -import { tracked } from '@glimmer/tracking'; -import { action } from '@ember/object'; -import Store from '@ember-data/store'; -import { task } from 'ember-concurrency'; +import { WithBoundArgs } from '@glint/template'; import ProjectModel from 'irene/models/project'; -import DevicePreferenceModel from 'irene/models/device-preference'; -import ProjectAvailableDeviceModel from 'irene/models/project-available-device'; +import ProjectPreferencesDevicePreferenceComponent from './device-preference'; export interface ProjectPreferencesSignature { Args: { @@ -22,142 +11,18 @@ export interface ProjectPreferencesSignature { platform?: number; }; Blocks: { - title: []; + default: [ + { + DevicePreferenceComponent: WithBoundArgs< + typeof ProjectPreferencesDevicePreferenceComponent, + 'dpContext' + >; + }, + ]; }; } -type EnumObject = { key: string; value: number | string }; -type DeviceType = EnumObject; - -export default class ProjectPreferencesComponent extends Component { - @service declare intl: IntlService; - @service declare ajax: any; - @service('notifications') declare notify: NotificationService; - @service declare store: Store; - - @tracked selectedVersion = '0'; - - @tracked selectedDeviceType?: DeviceType; - - @tracked deviceTypes = ENUMS.DEVICE_TYPE.CHOICES; - @tracked devicePreference?: DevicePreferenceModel; - - @tracked - devices: DS.AdapterPopulatedRecordArray | null = - null; - - get tAnyVersion() { - return this.intl.t('anyVersion'); - } - - constructor(owner: unknown, args: ProjectPreferencesSignature['Args']) { - super(owner, args); - - this.fetchDevicePreference.perform(); - this.fetchDevices.perform(); - } - - fetchDevicePreference = task(async () => { - this.devicePreference = await this.store.queryRecord('device-preference', { - id: this.args.profileId, - }); - - this.selectedDeviceType = this.filteredDeviceTypes.find( - (it) => it.value === this.devicePreference?.deviceType - ); - - this.selectedVersion = this.devicePreference.platformVersion; - }); - - fetchDevices = task(async () => { - this.devices = await this.store.query('project-available-device', { - projectId: this.args.project?.get('id'), - }); - }); - - get filteredDeviceTypes() { - return this.deviceTypes.filter( - (type) => ENUMS.DEVICE_TYPE.UNKNOWN !== type.value - ); - } - - get availableDevices() { - return this.devices?.filter( - (d) => d.platform === this.args.project?.get('platform') - ); - } - - get filteredDevices() { - return this.availableDevices?.filter((device) => { - switch (this.selectedDeviceType?.value) { - case ENUMS.DEVICE_TYPE.NO_PREFERENCE: - return true; - - case ENUMS.DEVICE_TYPE.TABLET_REQUIRED: - return device.isTablet; - - case ENUMS.DEVICE_TYPE.PHONE_REQUIRED: - return !device.isTablet; - - default: - return true; - } - }); - } - - get uniqueDevices() { - return this.filteredDevices?.uniqBy('platformVersion'); - } - - get devicePlatformVersionOptions() { - return ['0', ...(this.uniqueDevices?.map((d) => d.platformVersion) || [])]; - } - - @action - handleSelectDeviceType(deviceType: DeviceType) { - this.selectedDeviceType = deviceType; - this.selectedVersion = '0'; - - this.versionSelected.perform(); - } - - @action - handleSelectVersion(version: string) { - this.selectedVersion = version; - - this.versionSelected.perform(); - } - - versionSelected = task(async () => { - try { - const profileId = this.args.profileId; - - const devicePreferences = [ - ENV.endpoints['profiles'], - profileId, - ENV.endpoints['devicePreferences'], - ].join('/'); - - const data = { - device_type: this.selectedDeviceType?.value, - platform_version: this.selectedVersion, - }; - - await this.ajax.put(devicePreferences, { data }); - - if (!this.isDestroyed && this.devicePreference) { - this.devicePreference.deviceType = this.selectedDeviceType - ?.value as number; - - this.devicePreference.platformVersion = this.selectedVersion; - - this.notify.success(this.intl.t('savedPreferences')); - } - } catch (e) { - this.notify.error(this.intl.t('somethingWentWrong')); - } - }); -} +export default class ProjectPreferencesComponent extends Component {} declare module '@glint/environment-ember-loose/registry' { export default interface Registry { diff --git a/app/components/project-preferences/provider/index.hbs b/app/components/project-preferences/provider/index.hbs new file mode 100644 index 0000000000..a53fb6655d --- /dev/null +++ b/app/components/project-preferences/provider/index.hbs @@ -0,0 +1,11 @@ +{{yield + (hash + deviceTypes=this.filteredDeviceTypes + selectedDeviceType=this.selectedDeviceType + handleSelectDeviceType=this.handleSelectDeviceType + selectedVersion=this.selectedVersion + devicePlatformVersions=this.devicePlatformVersionOptions + handleSelectVersion=this.handleSelectVersion + isPreferredDeviceAvailable=this.isPreferredDeviceAvailable + ) +}} \ No newline at end of file diff --git a/app/components/project-preferences/provider/index.ts b/app/components/project-preferences/provider/index.ts new file mode 100644 index 0000000000..f712aeee71 --- /dev/null +++ b/app/components/project-preferences/provider/index.ts @@ -0,0 +1,214 @@ +import Component from '@glimmer/component'; +import { inject as service } from '@ember/service'; +import IntlService from 'ember-intl/services/intl'; +import { tracked } from '@glimmer/tracking'; +import { action } from '@ember/object'; +import Store from '@ember-data/store'; +import { task } from 'ember-concurrency'; + +// eslint-disable-next-line ember/use-ember-data-rfc-395-imports +import DS from 'ember-data'; + +import ENUMS from 'irene/enums'; +import ENV from 'irene/config/environment'; +import ProjectModel from 'irene/models/project'; +import DevicePreferenceModel from 'irene/models/device-preference'; +import ProjectAvailableDeviceModel from 'irene/models/project-available-device'; + +export interface DevicePreferenceContext { + deviceTypes: DeviceType[]; + selectedDeviceType?: DeviceType; + handleSelectDeviceType: (deviceType: DeviceType) => void; + selectedVersion: string; + devicePlatformVersions: string[]; + handleSelectVersion: (version: string) => void; + isPreferredDeviceAvailable: boolean | null; +} + +export interface ProjectPreferencesProviderSignature { + Args: { + project?: ProjectModel | null; + profileId?: number | string; + platform?: number; + }; + Blocks: { + default: [DevicePreferenceContext]; + }; +} + +type EnumObject = { key: string; value: number | string }; +type DeviceType = EnumObject; + +export default class ProjectPreferencesProviderComponent extends Component { + @service declare intl: IntlService; + @service declare ajax: any; + @service('notifications') declare notify: NotificationService; + @service declare store: Store; + + @tracked selectedVersion = '0'; + + @tracked selectedDeviceType?: DeviceType; + + @tracked deviceTypes = ENUMS.DEVICE_TYPE.CHOICES; + @tracked devicePreference?: DevicePreferenceModel; + + @tracked + devices: DS.AdapterPopulatedRecordArray | null = + null; + + constructor( + owner: unknown, + args: ProjectPreferencesProviderSignature['Args'] + ) { + super(owner, args); + + this.fetchDevicePreference.perform(); + this.fetchDevices.perform(); + } + + fetchDevicePreference = task(async () => { + this.devicePreference = await this.store.queryRecord('device-preference', { + id: this.args.profileId, + }); + + this.selectedDeviceType = this.filteredDeviceTypes.find( + (it) => it.value === this.devicePreference?.deviceType + ); + + this.selectedVersion = this.devicePreference.platformVersion; + }); + + fetchDevices = task(async () => { + this.devices = await this.store.query('project-available-device', { + projectId: this.args.project?.get('id'), + }); + }); + + get tAnyVersion() { + return this.intl.t('anyVersion'); + } + + get filteredDeviceTypes() { + return this.deviceTypes.filter( + (type) => ENUMS.DEVICE_TYPE.UNKNOWN !== type.value + ); + } + + get availableDevices() { + return this.devices?.filter( + (d) => d.platform === this.args.project?.get('platform') + ); + } + + get filteredDevices() { + return this.availableDevices?.filter((device) => { + switch (this.selectedDeviceType?.value) { + case ENUMS.DEVICE_TYPE.NO_PREFERENCE: + return true; + + case ENUMS.DEVICE_TYPE.TABLET_REQUIRED: + return device.isTablet; + + case ENUMS.DEVICE_TYPE.PHONE_REQUIRED: + return !device.isTablet; + + default: + return true; + } + }); + } + + get uniqueDevices() { + return this.filteredDevices?.uniqBy('platformVersion'); + } + + get devicePlatformVersionOptions() { + return ['0', ...(this.uniqueDevices?.map((d) => d.platformVersion) || [])]; + } + + get isPreferredDeviceAvailable() { + // check whether preferences & devices are resolved + if (this.devicePreference && this.uniqueDevices) { + const deviceType = Number(this.devicePreference.deviceType); + const version = this.devicePreference.platformVersion; + + // if both device type and os is any then return true + if (deviceType === 0 && version === '0') { + return true; + } + + // if os is any then return true + if (version === '0') { + return true; + } + + // check if preferred device type & os exists + return this.uniqueDevices.some((d) => { + // if only device type is any then just check version + if (deviceType === 0) { + return d.platformVersion === version; + } + + return ( + d.platformVersion === version && + (d.isTablet + ? deviceType === ENUMS.DEVICE_TYPE.TABLET_REQUIRED + : deviceType === ENUMS.DEVICE_TYPE.PHONE_REQUIRED) + ); + }); + } + + return null; + } + + @action + handleSelectDeviceType(deviceType: DeviceType) { + this.selectedDeviceType = deviceType; + this.selectedVersion = '0'; + + this.versionSelected.perform(); + } + + @action + handleSelectVersion(version: string) { + this.selectedVersion = version; + + this.versionSelected.perform(); + } + + versionSelected = task(async () => { + try { + const profileId = this.args.profileId; + + const devicePreferences = [ + ENV.endpoints['profiles'], + profileId, + ENV.endpoints['devicePreferences'], + ].join('/'); + + const data = { + device_type: this.selectedDeviceType?.value, + platform_version: this.selectedVersion, + }; + + await this.ajax.put(devicePreferences, { data }); + + if (!this.isDestroyed && this.devicePreference) { + this.devicePreference.deviceType = this.selectedDeviceType + ?.value as number; + + this.devicePreference.platformVersion = this.selectedVersion; + + this.notify.success(this.intl.t('savedPreferences')); + } + } catch (e) { + this.notify.error(this.intl.t('somethingWentWrong')); + } + }); +} + +declare module '@glint/environment-ember-loose/registry' { + export default interface Registry { + 'ProjectPreferences::Provider': typeof ProjectPreferencesProviderComponent; + } +} diff --git a/app/components/project-settings/general-settings/index.hbs b/app/components/project-settings/general-settings/index.hbs index 350dd8020e..b49068e1a5 100644 --- a/app/components/project-settings/general-settings/index.hbs +++ b/app/components/project-settings/general-settings/index.hbs @@ -10,12 +10,15 @@ @profileId={{@project.activeProfileId}} @platform={{@project.platform}} @project={{@project}} + as |pp| > - <:title> - - {{t 'devicePreferences'}} - - + + <:title> + + {{t 'devicePreferences'}} + + + diff --git a/mirage/factories/project-available-device.ts b/mirage/factories/project-available-device.ts index f0426296bf..da7762b30c 100644 --- a/mirage/factories/project-available-device.ts +++ b/mirage/factories/project-available-device.ts @@ -8,7 +8,7 @@ export default Base.extend({ }, platform_version() { - return faker.number.int(); + return `${faker.number.float({ min: 1, max: 50, fractionDigits: 2 })}`; }, is_tablet() { diff --git a/tests/integration/components/dynamic-scan-test.js b/tests/integration/components/dynamic-scan-test.js index 38dd9d2b75..383e5db47c 100644 --- a/tests/integration/components/dynamic-scan-test.js +++ b/tests/integration/components/dynamic-scan-test.js @@ -81,6 +81,8 @@ module( profile: profile.id, }); + const project = this.server.create('project', { file: file.id, id: '1' }); + const availableDevices = [ ...this.server.createList('project-available-device', 5, { is_tablet: true, @@ -100,12 +102,19 @@ module( }), ]; + // choose a random device for preference + const randomDevice = faker.helpers.arrayElement( + availableDevices.filter((it) => it.platform === project.platform) + ); + const devicePreference = this.server.create('device-preference', { id: profile.id, + device_type: randomDevice.is_tablet + ? ENUMS.DEVICE_TYPE.TABLET_REQUIRED + : ENUMS.DEVICE_TYPE.PHONE_REQUIRED, + platform_version: randomDevice.platform_version, }); - this.server.create('project', { file: file.id, id: '1' }); - this.setProperties({ file: store.push(store.normalize('file', file.toJSON())), dynamicScanText: 'Start', @@ -1030,5 +1039,95 @@ module( assert.dom('[data-test-dynamicScan-startBtn]').hasText('t:completed:()'); assert.dom('[data-test-dynamicScan-restartBtn]').exists(); }); + + test('test when preferred device is not available', async function (assert) { + const preferredDeviceType = this.devicePreference.device_type; + const preferredPlatformVersion = this.devicePreference.platform_version; + + // there can be duplicates + const preferredDeviceList = this.server.db.projectAvailableDevices.where( + (ad) => + ad.platform_version === preferredPlatformVersion && + (ad.is_tablet + ? preferredDeviceType === ENUMS.DEVICE_TYPE.TABLET_REQUIRED + : preferredDeviceType === ENUMS.DEVICE_TYPE.PHONE_REQUIRED) + ); + + // simulate preferred device not available + preferredDeviceList.forEach(({ id }) => { + this.server.db.projectAvailableDevices.remove(id); + }); + + const file = this.server.create('file', { + project: '1', + profile: '100', + dynamic_status: ENUMS.DYNAMIC_STATUS.NONE, + is_dynamic_done: false, + can_run_automated_dynamicscan: false, + is_active: true, + }); + + this.set( + 'file', + this.store.push(this.store.normalize('file', file.toJSON())) + ); + + this.server.get('/v2/projects/:id', (schema, req) => { + return schema.projects.find(`${req.params.id}`)?.toJSON(); + }); + + this.server.get('/profiles/:id', (schema, req) => + schema.profiles.find(`${req.params.id}`)?.toJSON() + ); + + this.server.get('/profiles/:id/device_preference', (schema, req) => { + return schema.devicePreferences.find(`${req.params.id}`)?.toJSON(); + }); + + this.server.get('/projects/:id/available-devices', (schema) => { + const results = schema.projectAvailableDevices.all().models; + + return { count: results.length, next: null, previous: null, results }; + }); + + this.server.get('/profiles/:id/proxy_settings', (_, req) => { + return { + id: req.params.id, + host: '', + port: '', + enabled: false, + }; + }); + + await render(hbs` + + `); + + assert + .dom('[data-test-dynamicScan-startBtn]') + .hasText(this.dynamicScanText); + + assert.dom('[data-test-dynamicScanModal-startBtn]').doesNotExist(); + + await click('[data-test-dynamicScan-startBtn]'); + + assert + .dom( + `[data-test-projectPreference-deviceTypeSelect] .${classes.trigger}` + ) + .hasClass(classes.triggerError); + + assert + .dom( + `[data-test-projectPreference-osVersionSelect] .${classes.trigger}` + ) + .hasClass(classes.triggerError); + + assert + .dom('[data-test-projectPreference-deviceUnavailableError]') + .hasText('t:modalCard.dynamicScan.preferredDeviceNotAvailable:()'); + + assert.dom('[data-test-dynamicScanModal-startBtn]').isDisabled(); + }); } ); diff --git a/translations/en.json b/translations/en.json index fb6cf1d49d..f5b330b876 100644 --- a/translations/en.json +++ b/translations/en.json @@ -608,6 +608,7 @@ "processorArchitecture": "Processor architecture", "deviceTypes": "Device types", "deviceSettingsWarning": "Please ensure the mobile device settings remain unchanged during the dynamic scan. Altering device settings outside your application can cause the scanner to malfunction. Kindly refrain from actions such as updating the OS, disconnecting from WiFi, performing factory resets, uninstalling apps or changing system settings.", + "preferredDeviceNotAvailable": "Preferred device type and OS is not available.", "runApiScan": "Enable API Capture", "apiScanDescription": "During dynamic scan network traffic made by the app, filtered by the below provided API endpoints will be recorded. Currently traffic capturing is not supported for the apps that employ SSL pinning.", "start": "Start Dynamic Scan" diff --git a/translations/ja.json b/translations/ja.json index bc74080b49..feb0c07cab 100644 --- a/translations/ja.json +++ b/translations/ja.json @@ -608,6 +608,7 @@ "processorArchitecture": "Processor architecture", "deviceTypes": "Device types", "deviceSettingsWarning": "Please ensure the mobile device settings remain unchanged during the dynamic scan. Altering device settings outside your application can cause the scanner to malfunction. Kindly refrain from actions such as updating the OS, disconnecting from WiFi, performing factory resets, uninstalling apps or changing system settings.", + "preferredDeviceNotAvailable": "Preferred device type and OS is not available.", "runApiScan": "Enable API Capture", "apiScanDescription": "During dynamic scan network traffic made by the app, filtered by the below provided API endpoints will be recorded. Currently traffic capturing is not supported for the apps that employ SSL pinning.", "start": "Start Dynamic Scan"