Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: add qr scanner #357

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -80,3 +80,5 @@ buck-out/

# General
.env

/resources/html/*.html
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,11 @@ npm install

Once all dependencies are installed and your target development environments are setup (Xcode for iOS and Android Studio for Android), it should be possible to begin development with virtual devices.

Before starting the application, make sure to build required HTML file for Camera QR Webview with the following command:
```shell
npm run build:webview
QuinsZouls marked this conversation as resolved.
Show resolved Hide resolved
```

**Important note about Node.js support**: Development for this project should be performed on Node version 8. Although it may work on versions 6 and newer, we will not be supporting issues raised for these versions. Similarly, we do not currently support NodeJS version 9.

### iOS development
Expand Down
3 changes: 3 additions & 0 deletions android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,9 @@ android {
}
}
}
sourceSets {
main { assets.srcDirs = ['../../resources/html'] }
}
}

dependencies {
Expand Down
1 change: 1 addition & 0 deletions android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
<uses-permission android:name="android.permission.USE_FINGERPRINT" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.CAMERA" />

<application
android:name=".MainApplication"
Expand Down
13 changes: 10 additions & 3 deletions ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,9 @@ PODS:
- React-Core
- react-native-slider (4.4.3):
- React-Core
- react-native-webview (13.8.4):
- RCT-Folly (= 2021.07.22.00)
- React-Core
- React-NativeModulesApple (0.72.7):
- hermes-engine
- React-callinvoker
Expand Down Expand Up @@ -518,7 +521,7 @@ PODS:
- RNScreens (3.27.0):
- RCT-Folly (= 2021.07.22.00)
- React-Core
- RNSVG (13.13.0):
- RNSVG (13.14.0):
- React-Core
- SocketRocket (0.6.1)
- TouchID (4.4.1):
Expand Down Expand Up @@ -579,6 +582,7 @@ DEPENDENCIES:
- react-native-randombytes (from `../node_modules/react-native-randombytes`)
- react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`)
- "react-native-slider (from `../node_modules/@react-native-community/slider`)"
- react-native-webview (from `../node_modules/react-native-webview`)
- React-NativeModulesApple (from `../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios`)
- React-perflogger (from `../node_modules/react-native/ReactCommon/reactperflogger`)
- React-RCTActionSheet (from `../node_modules/react-native/Libraries/ActionSheetIOS`)
Expand Down Expand Up @@ -680,6 +684,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native-safe-area-context"
react-native-slider:
:path: "../node_modules/@react-native-community/slider"
react-native-webview:
:path: "../node_modules/react-native-webview"
React-NativeModulesApple:
:path: "../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios"
React-perflogger:
Expand Down Expand Up @@ -776,6 +782,7 @@ SPEC CHECKSUMS:
react-native-randombytes: 421f1c7d48c0af8dbcd471b0324393ebf8fe7846
react-native-safe-area-context: 2cd91d532de12acdb0a9cbc8d43ac72a8e4c897c
react-native-slider: 1cdd6ba29675df21f30544253bf7351d3c2d68c4
react-native-webview: fa228e55c53372c2b361d2fa5e415844fa83eabf
React-NativeModulesApple: b6868ee904013a7923128892ee4a032498a1024a
React-perflogger: 31ea61077185eb1428baf60c0db6e2886f141a5a
React-RCTActionSheet: 392090a3abc8992eb269ef0eaa561750588fc39d
Expand All @@ -801,7 +808,7 @@ SPEC CHECKSUMS:
RNGestureHandler: 32a01c29ecc9bb0b5bf7bc0a33547f61b4dc2741
RNReanimated: fdbaa9c964bbab7fac50c90862b6cc5f041679b9
RNScreens: 3c2d122f5e08c192e254c510b212306da97d2581
RNSVG: ed492aaf3af9ca01bc945f7a149d76d62e73ec82
RNSVG: d00c8f91c3cbf6d476451313a18f04d220d4f396
SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17
TouchID: ba4c656d849cceabc2e4eef722dea5e55959ecf4
Yoga: 4c3aa327e4a6a23eeacd71f61c81df1bcdf677d5
Expand All @@ -810,4 +817,4 @@ SPEC CHECKSUMS:

PODFILE CHECKSUM: e794ec0db9f2599d024202d7e54a25d5dcc8571d

COCOAPODS: 1.11.3
COCOAPODS: 1.15.2
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"scripts": {
"android": "react-native run-android",
"build:ios": "react-native bundle --entry-file='index.js' --bundle-output='./ios/main.jsbundle' --dev=false --platform='ios'",
"build:webview": "node ./scripts/build_qrview.js",
"format": "prettier --write \"{{source,test}/**/*.{js,ts,jsx,tsx},*.{js,ts,jsx,tsx}}\"",
"ios": "react-native run-ios --simulator=\"iPhone Xs\"",
"lint": "eslint . --ext .js,.jsx,.ts,.tsx",
Expand Down Expand Up @@ -71,6 +72,7 @@
"react-native-touch-id": "^4.4.1",
"react-native-url-polyfill": "^1.3.0",
"react-native-uuid": "^2.0.1",
"react-native-webview": "^13.8.4",
"react-obstate": "^0.1.3",
"stream-browserify": "^3.0.0",
"url-join": "^4.0.1",
Expand All @@ -96,6 +98,7 @@
"eslint": "^8.55.0",
"eslint-plugin-prettier": "^5.0.1",
"jest": "^29.2.1",
"jsqr": "^1.4.0",
"metro-react-native-babel-preset": "^0.77.0",
"prettier": "^3.1.0",
"react-native-svg-transformer": "^1.1.0",
Expand Down
Empty file added resources/html/.gitkeep
Empty file.
81 changes: 81 additions & 0 deletions scripts/QRScanner.template.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>OTP Scanner</title>
<style>
* {
margin: 0;
padding: 0;
overflow: hidden;
}

main {
height: 100vh;
width: 100%;
border-radius: 15px;
}

#preview {
width: 100%;
height: 100%;
position: relative;
}

video {
width: 100%;
height: 100%;
object-fit: cover;

}
</style>
<script>
{ { jsQRScript } }
</script>
</head>

<body>
<main>
<div id="preview">
<video id="video" autoplay muted loop playsinline></video>
<canvas id="canvas" hidden></canvas>
</div>
</main>
<script>
const video = document.getElementById('video');
const canvas = document.getElementById('canvas');
const context = canvas.getContext('2d');
navigator.mediaDevices.getUserMedia({ video: { facingMode: 'environment' } })
.then(function (stream) {
video.srcObject = stream;
video.play();
requestAnimationFrame(tick);
});
function tick() {
if (video.readyState === video.HAVE_ENOUGH_DATA) {
canvas.height = video.videoHeight;
canvas.width = video.videoWidth;

context.drawImage(video, 0, 0, canvas.width, canvas.height);

var imageData = context.getImageData(0, 0, canvas.width, canvas.height);
var code = jsQR(imageData.data, imageData.width, imageData.height, {
inversionAttempts: "dontInvert",
});

if (code) {
console.log("Found QR code", code.data);
if (window.ReactNativeWebView) {
window.ReactNativeWebView.postMessage(code.data)
}
}
}

requestAnimationFrame(tick);
}
</script>
</body>

</html>
19 changes: 19 additions & 0 deletions scripts/build_qrview.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
const fs = require("fs");
const path = require("path");

const jsqrPath = path.join(__dirname, "../node_modules/jsqr/dist/jsQR.js");
QuinsZouls marked this conversation as resolved.
Show resolved Hide resolved

const htmlPath = path.join(__dirname, "./QRScanner.template.html");

const qrViewPath = path.join(__dirname, "../resources/html/OTPScanner.html");

async function buildQrView() {
const jsqr = fs.readFileSync(jsqrPath, "utf8");
const html = fs.readFileSync(htmlPath, "utf8");

const qrView = html.replace("{ { jsQRScript } }", jsqr);

fs.writeFileSync(qrViewPath, qrView);
}

buildQrView();
60 changes: 60 additions & 0 deletions source/components/modals/OTPScannerModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { Modal } from "@ui-kitten/components";
import { WebView } from "react-native-webview";
import { Platform, StyleSheet, View } from "react-native";

export interface OTPScannerModalProps {
visible: boolean;
onBackdropPress: () => void;
onScan: (data: string) => void;
}

function getDeviceSource() {
if (Platform.OS === "android") {
return { uri: "file:///android_asset/OTPScanner.html" };
}
const template = require("../../../resources/html/OTPScanner.html");

return template;
}

export function OTPScannerModal({ visible, onBackdropPress, onScan }: OTPScannerModalProps) {
return (
<Modal
visible={visible}
backdropStyle={styles.backdrop}
onBackdropPress={() => onBackdropPress()}
>
<View style={styles.container}>
<WebView
mediaPlaybackRequiresUserAction={false}
mediaCapturePermissionGrantType="grant"
javaScriptEnabled={true}
originWhitelist={["*"]}
source={getDeviceSource()}
style={styles.webview}
allowsInlineMediaPlayback
onMessage={event => {
onScan(event.nativeEvent.data);
}}
/>
</View>
</Modal>
);
}

const styles = StyleSheet.create({
container: {
flex: 1,
display: "flex",
borderRadius: 15,
overflow: "hidden",
minHeight: 300,
width: 300
},
backdrop: {
backgroundColor: "rgba(0, 0, 0, 0.5)"
},
webview: {
flex: 1
}
});
36 changes: 36 additions & 0 deletions source/components/screens/EditEntryScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,14 @@ import { GENERATOR, GeneratorMode } from "../../state/generator";
import { getEntryFacade, saveExistingEntryChanges, saveNewEntry } from "../../services/buttercup";
import { setBusyState } from "../../services/busyState";
import { OTP } from "../../types";
import { OTPScannerModal } from "../modals/OTPScannerModal";

const BackIcon = props => <Icon {...props} name="arrow-back" />;
// const DeleteIcon = props => <Icon {...props} name="trash-2-outline" />;
const ModifyIcon = props => <Icon {...props} name="settings-outline" />;
const SaveIcon = props => <Icon {...props} name="save-outline" />;
const TitleIcon = props => <Icon {...props} name="text-outline" />;
const CameraIcon = props => <Icon {...props} name="camera-outline" />;

interface FieldEditMenuButtonProps {
entryID: EntryID;
Expand Down Expand Up @@ -111,6 +113,11 @@ const styles = StyleSheet.create({
width: 36,
height: 20
},
cameraButton: {
marginLeft: 6,
width: 36,
height: 20
},
passwordInput: {
fontFamily: MONO_FONT
},
Expand Down Expand Up @@ -169,6 +176,28 @@ function FieldEditMenuButton(props: FieldEditMenuButtonProps) {
);
}

function OTPScannerButton({ onScanSuccess }: { onScanSuccess: (data: string) => void }) {
const [visible, setVisible] = useState(false);
return (
<>
<OTPScannerModal
visible={visible}
onBackdropPress={() => setVisible(false)}
onScan={response => {
onScanSuccess(response);
setVisible(false);
}}
/>
<Button
accessoryLeft={CameraIcon}
onPress={() => setVisible(true)}
status="control"
style={styles.cameraButton}
/>
</>
);
}

export function EditEntryScreen({ navigation, route }) {
const { entryID = null, entryType, groupID } = route?.params ?? {};
const [currentSource] = useSingleState(VAULT, "currentSource");
Expand Down Expand Up @@ -398,6 +427,13 @@ export function EditEntryScreen({ navigation, route }) {
}}
value={field.value}
/>
{field.valueType == EntryPropertyValueType.OTP && (
<OTPScannerButton
onScanSuccess={data =>
handleFieldValueChange(field.id, data)
}
/>
)}
<FieldEditMenuButton
entryID={entryID}
items={[
Expand Down