-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
22 changed files
with
376 additions
and
8 deletions.
There are no files selected for viewing
83 changes: 83 additions & 0 deletions
83
libraries/commerce/scanner/actions/grantCameraPermissions.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
163
libraries/commerce/scanner/actions/grantCameraPermissions.spec.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.