Skip to content

Commit

Permalink
[CityOfZion#1318] Actual 1/2: Enable “Scan QR” button only if camera …
Browse files Browse the repository at this point in the history
…available
  • Loading branch information
ranbena committed Nov 28, 2018
1 parent 57ddc3d commit 4ffa776
Show file tree
Hide file tree
Showing 5 changed files with 127 additions and 5 deletions.
1 change: 1 addition & 0 deletions __tests__/components/__snapshots__/Login.test.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ exports[`Login renders without crashing 1`] = `
className="privateKeyLoginButtonRow"
>
<Button
disabled={true}
id="scan-private-key-qr-button"
onClick={[Function]}
primary={true}
Expand Down
6 changes: 4 additions & 2 deletions app/containers/LoginPrivateKey/LoginPrivateKey.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ import Close from '../../assets/icons/close.svg'
import styles from '../Home/Home.scss'

type Props = {
loginWithPrivateKey: Function
loginWithPrivateKey: Function,
cameraAvailable: boolean
}

type State = {
Expand All @@ -35,7 +36,7 @@ export default class LoginPrivateKey extends React.Component<Props, State> {
}

render = () => {
const { loginWithPrivateKey } = this.props
const { loginWithPrivateKey, cameraAvailable } = this.props
const { wif, scannerActive } = this.state

return (
Expand Down Expand Up @@ -84,6 +85,7 @@ export default class LoginPrivateKey extends React.Component<Props, State> {
primary
renderIcon={GridIcon}
onClick={this.toggleScanner}
disabled={!cameraAvailable}
>
Scan QR
</Button>
Expand Down
4 changes: 3 additions & 1 deletion app/containers/LoginPrivateKey/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { withActions } from 'spunky'

import LoginPrivateKey from './LoginPrivateKey'
import withFailureNotification from '../../hocs/withFailureNotification'
import withCameraAvailability from '../../hocs/withCameraAvailability'
import { wifLoginActions } from '../../actions/authActions'

const mapActionsToProps = actions => ({
Expand All @@ -12,5 +13,6 @@ const mapActionsToProps = actions => ({

export default compose(
withActions(wifLoginActions, mapActionsToProps),
withFailureNotification(wifLoginActions)
withFailureNotification(wifLoginActions),
withCameraAvailability
)(LoginPrivateKey)
99 changes: 99 additions & 0 deletions app/hocs/withCameraAvailability.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
// @flow
import React from 'react'
import { filter } from 'lodash-es'
import { cancellablePoll as poll } from '../util/poll'
import type { CancellableType } from '../util/poll'

const POLL_FREQUENCY = 1000

type State = {
avail: boolean
}

type Props = {}

// $FlowFixMe
export default function withCameraAvailability(Component) {
class CameraAvailability extends React.Component<Props, State> {
config = { frequency: POLL_FREQUENCY }

cancellable: CancellableType

state = {
avail: false
}

componentDidMount() {
// start wait for available camera
this.awaitAvailable()
}

componentDidUpdate(_: Props, prevState: State) {
// cleanup
this.cancellable.cancel()

// if camera found, wait until no longer available
// if no camera found, wait until available
if (prevState.avail) {
this.awaitNotAvailable()
} else {
this.awaitAvailable()
}
}

componentWillUnmount() {
// cancel current poll
this.cancellable.cancel()
}

// wait till camera is available
awaitAvailable = async (): Promise<*> => {
// register poll to allow cancellation
this.cancellable = poll(this.isCameraAvailable, this.config)

// once resolved, update the avail state
return this.cancellable.promise.then(() => {
this.setState({ avail: true })
})
}

// wait till camera is no longer available
awaitNotAvailable = async (): Promise<*> => {
// register poll to allow cancellation
this.cancellable = poll(this.noCameraAvailable, this.config)

// once resolved, update the avail state
return this.cancellable.promise.then(() => {
this.setState({ avail: false })
})
}

// check if any camera devices are available
isCameraAvailable = async (): Promise<*> => {
// get available cameras
const cameras = await this.getCameras()
// resolve if found camera, reject if not
return cameras.length > 0 ? Promise.resolve() : Promise.reject()
}

// check if no camera devices are available
noCameraAvailable = async (): Promise<*> => {
// get available cameras
const cameras = await this.getCameras()
// reject if found camera, resolve if not
return cameras.length === 0 ? Promise.resolve() : Promise.reject()
}

// get available cameras
getCameras = async (): Promise<Array<MediaDeviceInfoType>> => {
const devices = await navigator.mediaDevices.enumerateDevices()
return filter(devices, { kind: 'videoinput' })
}

render() {
return <Component cameraAvailable={this.state.avail} {...this.props} />
}
}

return CameraAvailability
}
22 changes: 20 additions & 2 deletions flow-typed/declarations.js
Original file line number Diff line number Diff line change
Expand Up @@ -122,5 +122,23 @@ declare type SendEntryType = {
symbol: SymbolType
}


declare type ThemeType = THEME.LIGHT | THEME.DARK
declare type ThemeType = THEME.LIGHT | THEME.DARK

// polyfill flow's bom since Node does implement navigator.mediaDevices
declare interface Navigator extends Navigator {
mediaDevices: MediaDevicesType;
}

// https://mdn.io/MediaDevices
// https://mdn.io/MediaDevices/enumerateDevices
declare type MediaDevicesType = {
enumerateDevices(): Promise<Array<MediaDeviceInfoType>>
}

// https://mdn.io/MediaDeviceInfo
declare type MediaDeviceInfoType = {
deviceId: string,
groupId: string,
kind: 'videoinput' | 'audioinput' | 'audiooutput',
label: string
}

0 comments on commit 4ffa776

Please sign in to comment.