Skip to content

Commit

Permalink
Merge 4f82377 into 21e126d
Browse files Browse the repository at this point in the history
  • Loading branch information
fkloes committed Apr 4, 2019
2 parents 21e126d + 4f82377 commit 9fd997c
Show file tree
Hide file tree
Showing 22 changed files with 376 additions and 8 deletions.
83 changes: 83 additions & 0 deletions libraries/commerce/scanner/actions/grantCameraPermissions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import {
event,
openAppSettings,
getAppPermissions,
requestAppPermissions,
APP_EVENT_APPLICATION_WILL_ENTER_FOREGROUND,
PERMISSION_ID_CAMERA,
STATUS_DENIED,
STATUS_GRANTED,
STATUS_NOT_DETERMINED,
STATUS_NOT_SUPPORTED,
} from '@shopgate/pwa-core';
import showModal from '@shopgate/pwa-common/actions/modal/showModal';

/**
* Grant camera permissions.
* @return { Function } A redux thunk.
*/
export default () => dispatch => new Promise(async (resolve) => {
let status;

// Check the current status of the camera permissions.
[{ status }] = await getAppPermissions([PERMISSION_ID_CAMERA]);

// Stop the process when the permission type is not supported.
if (status === STATUS_NOT_SUPPORTED) {
resolve(false);
return;
}

// The user never seen the permissions dialog yet, or temporary denied the permissions (Android).
if (status === STATUS_NOT_DETERMINED) {
// Trigger the native permissions dialog.
[{ status }] = await requestAppPermissions([{ permissionId: PERMISSION_ID_CAMERA }]);

// The user denied the permissions within the native dialog.
if ([STATUS_DENIED, STATUS_NOT_DETERMINED].includes(status)) {
resolve(false);
return;
}
}

if (status === STATUS_GRANTED) {
resolve(true);
return;
}

// The user permanently denied the permissions before.
if (status === STATUS_DENIED) {
// Present a modal that describes the situation, and allows the user to enter the app settings.
const openSettings = await dispatch(showModal({
title: null,
message: 'scanner.camera_access_denied.message',
confirm: 'scanner.camera_access_denied.settings_button',
}));

// The user just closed the modal.
if (!openSettings) {
resolve(false);
return;
}

/**
* Handler for the app event.
*/
const handler = async () => {
event.removeCallback(APP_EVENT_APPLICATION_WILL_ENTER_FOREGROUND, handler);
[{ status }] = await getAppPermissions([PERMISSION_ID_CAMERA]);
resolve(status === STATUS_GRANTED);
};

/**
* Register an event handler, so that we can perform the permissions check again,
* when the user comes back from the settings.
*/
event.addCallback(APP_EVENT_APPLICATION_WILL_ENTER_FOREGROUND, handler);

// Open the settings (protected by a timeout, so that the modal closes before the app is left).
setTimeout(() => {
openAppSettings();
}, 0);
}
});
163 changes: 163 additions & 0 deletions libraries/commerce/scanner/actions/grantCameraPermissions.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import {
event,
getAppPermissions,
requestAppPermissions,
openAppSettings,
STATUS_DENIED,
STATUS_GRANTED,
STATUS_NOT_DETERMINED,
STATUS_NOT_SUPPORTED,
APP_EVENT_APPLICATION_WILL_ENTER_FOREGROUND,
} from '@shopgate/pwa-core';
import showModal from '@shopgate/pwa-common/actions/modal/showModal';

import grantCameraPermissions from './grantCameraPermissions';

jest.unmock('@shopgate/pwa-core');
jest.mock('@shopgate/pwa-core/classes/Event');
jest.mock('@shopgate/pwa-core/commands/openAppSettings');
jest.mock('@shopgate/pwa-core/commands/appPermissions', () => ({
getAppPermissions: jest.fn(),
requestAppPermissions: jest.fn(),
}));
jest.mock('@shopgate/pwa-common/actions/modal/showModal', () => jest.fn());

/**
* @param {string} status The desired permission status.
* @returns {Array}
*/
const getPermissionsResponse = (status = STATUS_GRANTED) => [{ status }];

/**
* Flushes the promise queue.
* @returns {Promise}
*/
const flushPromises = () => new Promise(resolve => setImmediate(resolve));

describe('grantCameraPermissions', () => {
const dispatch = jest.fn(action => action);
jest.useFakeTimers();

beforeAll(() => {
getAppPermissions.mockResolvedValue(getPermissionsResponse(STATUS_GRANTED));
requestAppPermissions.mockResolvedValue(getPermissionsResponse(STATUS_GRANTED));
showModal.mockResolvedValue(true);
});

beforeEach(() => {
jest.clearAllMocks();
event.removeAllListeners();
});

it('should resolve with TRUE when the camera permissions are granted', async () => {
const granted = await grantCameraPermissions()(dispatch);

expect(granted).toBe(true);
expect(dispatch).not.toHaveBeenCalled();
expect(openAppSettings).not.toHaveBeenCalled();
expect(event.addCallbackSpy).not.toHaveBeenCalled();
});

it('should resolve with FALSE when the camera permissions are not supported', async () => {
getAppPermissions.mockResolvedValueOnce(getPermissionsResponse(STATUS_NOT_SUPPORTED));

const granted = await grantCameraPermissions()(dispatch);
expect(granted).toBe(false);
expect(dispatch).not.toHaveBeenCalled();
expect(openAppSettings).not.toHaveBeenCalled();
expect(event.addCallbackSpy).not.toHaveBeenCalled();
});

it('should resolve with TRUE when the camera permissions where not determined, but the user granted them', async () => {
getAppPermissions.mockResolvedValueOnce(getPermissionsResponse(STATUS_NOT_DETERMINED));

const granted = await grantCameraPermissions()(dispatch);
expect(granted).toBe(true);
expect(dispatch).not.toHaveBeenCalled();
expect(openAppSettings).not.toHaveBeenCalled();
expect(event.addCallbackSpy).not.toHaveBeenCalled();
});

it('should resolve with FALSE when the camera permissions where not determined, and the user denied them', async () => {
getAppPermissions.mockResolvedValueOnce(getPermissionsResponse(STATUS_NOT_DETERMINED));
requestAppPermissions.mockResolvedValue(getPermissionsResponse(STATUS_DENIED));

const granted = await grantCameraPermissions()(dispatch);
expect(granted).toBe(false);
expect(dispatch).not.toHaveBeenCalled();
expect(openAppSettings).not.toHaveBeenCalled();
expect(event.addCallbackSpy).not.toHaveBeenCalled();
});

it('should resolve with FALSE when the camera permissions where not determined, and the user denied them temporary', async () => {
getAppPermissions.mockResolvedValueOnce(getPermissionsResponse(STATUS_NOT_DETERMINED));
requestAppPermissions.mockResolvedValue(getPermissionsResponse(STATUS_NOT_DETERMINED));
const granted = await grantCameraPermissions()(dispatch);
expect(granted).toBe(false);
expect(dispatch).not.toHaveBeenCalled();
expect(openAppSettings).not.toHaveBeenCalled();
expect(event.addCallbackSpy).not.toHaveBeenCalled();
});

it('should resolve with FALSE when the user denied to open the app settings', async () => {
getAppPermissions.mockResolvedValueOnce(getPermissionsResponse(STATUS_DENIED));
showModal.mockResolvedValueOnce(false);

const granted = await grantCameraPermissions()(dispatch);
expect(granted).toBe(false);
expect(dispatch).toHaveBeenCalledTimes(1);
expect(showModal).toHaveBeenCalledWith({
title: null,
message: 'scanner.camera_access_denied.message',
confirm: 'scanner.camera_access_denied.settings_button',
});
expect(openAppSettings).not.toHaveBeenCalled();
expect(event.addCallbackSpy).not.toHaveBeenCalled();
});

it('should resolve with FALSE when the user opened the settings, but did not granted permissions', (done) => {
getAppPermissions.mockResolvedValueOnce(getPermissionsResponse(STATUS_DENIED));
getAppPermissions.mockResolvedValueOnce(getPermissionsResponse(STATUS_DENIED));

grantCameraPermissions()(dispatch)
.then((granted) => {
expect(granted).toBe(false);
expect(dispatch).toHaveBeenCalledTimes(1);
expect(openAppSettings).toHaveBeenCalledTimes(1);
expect(event.removeCallbackSpy).toHaveBeenCalledWith(
APP_EVENT_APPLICATION_WILL_ENTER_FOREGROUND,
expect.any(Function)
);
done();
});

// Flush the promise queue, so that the code inside of promise from the action is executed.
flushPromises().then(() => {
event.call([APP_EVENT_APPLICATION_WILL_ENTER_FOREGROUND]);
jest.runAllTimers();
});
});

it('should resolve with TRUE when the user opened the settings,and granted permissions', (done) => {
getAppPermissions.mockResolvedValueOnce(getPermissionsResponse(STATUS_DENIED));
getAppPermissions.mockResolvedValueOnce(getPermissionsResponse(STATUS_GRANTED));

grantCameraPermissions()(dispatch)
.then((granted) => {
expect(granted).toBe(true);
expect(dispatch).toHaveBeenCalledTimes(1);
expect(openAppSettings).toHaveBeenCalledTimes(1);
expect(event.removeCallbackSpy).toHaveBeenCalledWith(
APP_EVENT_APPLICATION_WILL_ENTER_FOREGROUND,
expect.any(Function)
);
done();
});

// Flush the promise queue, so that the code inside of promise from the action is executed.
flushPromises().then(() => {
event.call([APP_EVENT_APPLICATION_WILL_ENTER_FOREGROUND]);
jest.runAllTimers();
});
});
});
4 changes: 2 additions & 2 deletions libraries/core/classes/ScannerManager/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ let eventsRegistered = false;
*/

/**
* The ScannerManager class. It's intendend to simplify the processes that are necessary to
* The ScannerManager class. It's intended to simplify the processes that are necessary to
* programmatically interact with the scanner feature of the app. It provides the possibility to
* register a handler callback to process the scanned content.
* @deprecated This is a legacy implementation. Use Scanner instead.
Expand Down Expand Up @@ -128,7 +128,7 @@ class ScannerManager {
* Register a handler to process scanned content. Errors that are thrown inside will be displayed
* to the user as a notification, so that the webview can stay open for further scan attempts.
* It's recommended to use the ScanProcessingError for that purpose,
* since it provides the possiblity to set a message and a title for the notification.
* since it provides the possibility to set a message and a title for the notification.
* @param {scanHandler} handler The callback - async functions are supported.
* @return {ScannerManager}
*/
Expand Down
19 changes: 19 additions & 0 deletions libraries/core/commands/__tests__/openAppSettings.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// eslint-disable-next-line import/named
import { mockedSetCommandName, mockedSetCommandParams, mockedDispatch } from '../../classes/AppCommand';
import openAppSettings from '../openAppSettings';

jest.mock('../../classes/AppCommand');

describe('openAppSettings command', () => {
beforeEach(() => {
jest.clearAllMocks();
});

it('should dispatch', () => {
openAppSettings();

expect(mockedSetCommandName).toHaveBeenCalledWith('openAppSettings');
expect(mockedSetCommandParams).not.toHaveBeenCalled();
expect(mockedDispatch).toHaveBeenCalledWith(undefined);
});
});
4 changes: 2 additions & 2 deletions libraries/core/commands/appPermissions.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import RequestAppPermissionsRequest from '../classes/AppPermissionsRequest/Reque

/**
* Gathers the current permissions from the operating system.
* @param {Array} [permissionIds=[]] The desired permission ids. If kept empty all will be retured.
* @param {Array} [permissionIds=[]] The desired permission ids. If kept empty all will be returned.
* @return {Promise<Array>}
*/
export const getAppPermissions = (permissionIds = []) =>
Expand All @@ -13,7 +13,7 @@ export const getAppPermissions = (permissionIds = []) =>

/**
* Requests additional permissions from the operating system.
* @param {Array} permissions The desired permissions. If kept empty all will be retured.
* @param {Array} permissions The desired permissions. If kept empty all will be returned.
* @return {Promise<Array>}
*/
export const requestAppPermissions = permissions =>
Expand Down
12 changes: 12 additions & 0 deletions libraries/core/commands/openAppSettings.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import AppCommand from '../classes/AppCommand';

/**
* Sends an openAppSettings command to the app.
*/
export default function openAppSettings() {
const command = new AppCommand();

command
.setCommandName('openAppSettings')
.dispatch();
}
2 changes: 2 additions & 0 deletions libraries/core/constants/AppEvents.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
export const EVENT_KEYBOARD_WILL_CHANGE = 'keyboardWillChange';

export const APP_EVENT_APPLICATION_WILL_ENTER_FOREGROUND = 'applicationWillEnterForeground';

export const APP_EVENT_SCANNER_DID_SCAN = 'scannerDidScan';
export const APP_EVENT_SCANNER_DID_APPEAR = 'scannerDidAppear';
export const APP_EVENT_SCANNER_DID_DISAPPEAR = 'scannerDidDisappear';
1 change: 1 addition & 0 deletions libraries/core/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export { default as hideMenuBar } from './commands/hideMenuBar';
export { default as hideNavigationBar } from './commands/hideNavigationBar';
export { default as hideSplashScreen } from './commands/hideSplashScreen';
export { default as onload } from './commands/onload';
export { default as openAppSettings } from './commands/openAppSettings';
export { default as openPage } from './commands/openPage';
export { default as openPageExtern } from './commands/openPageExtern';
export {
Expand Down
4 changes: 4 additions & 0 deletions themes/theme-gmd/locale/de-DE.json
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,10 @@
"noResult": {
"qrCode": "Wir konnten für diesen QR-Code kein Ergebnis finden.",
"barCode": "Produkt nicht gefunden."
},
"camera_access_denied": {
"message": "Diese Funktion benötigt Zugriff auf die Kamera. Bitte erlauben Sie den Zugriff in den Einstellungen.",
"settings_button": "Zu den Einstellungen"
}
}
}
4 changes: 4 additions & 0 deletions themes/theme-gmd/locale/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,10 @@
"noResult": {
"qrCode": "We couldn't find a result for this QR code.",
"barCode": "Product not found."
},
"camera_access_denied": {
"message": "This feature requires access to your camera. Please allow access in the settings.",
"settings_button": "Go to settings"
}
}
}
4 changes: 4 additions & 0 deletions themes/theme-gmd/locale/es-ES.json
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,10 @@
"noResult": {
"qrCode": "No pudimos encontrar un resultado para este código QR.",
"barCode": "No se encontraron productos."
},
"camera_access_denied": {
"message": "Esta característica requiere acceso a su cámara. Por favor, permita el acceso desde la configuración",
"settings_button": "Ir a la configuración"
}
}
}
4 changes: 4 additions & 0 deletions themes/theme-gmd/locale/fr-FR.json
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,10 @@
"noResult": {
"qrCode": "Nous n'avons pas trouvé de résultat pour ce code QR.",
"barCode": "Aucun produit trouvé."
},
"camera_access_denied": {
"message": "Cette fonctionnalité nécessite un accès à votre appareil photo. Veuillez autoriser l'accès dans la fonction paramètres.",
"settings_button": "Aller dans paramètres"
}
}
}
4 changes: 4 additions & 0 deletions themes/theme-gmd/locale/it-IT.json
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,10 @@
"noResult": {
"qrCode": "Non siamo riusciti a trovare alcun risultato per questo codice QR.",
"barCode": "Nessun prodotto trovato."
},
"camera_access_denied": {
"message": "Questa funzione richiede l'accesso alla tua fotocamera. Consenti l'accesso nelle impostazioni per favore.",
"settings_button": "Vai alle impostazioni"
}
}
}
4 changes: 4 additions & 0 deletions themes/theme-gmd/locale/nl-NL.json
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,10 @@
"noResult": {
"qrCode": "Er werd geen resultaat voor uw QR code gevonden.",
"barCode": "Geen producten gevonden."
},
"camera_access_denied": {
"message": "Deze functie vereist toegang tot uw camera. Gelieve toegang te verlenen in de instellingen.",
"settings_button": "Ga naar instellingen"
}
}
}

0 comments on commit 9fd997c

Please sign in to comment.