Skip to content

Commit

Permalink
feat: support login using QR code (#26)
Browse files Browse the repository at this point in the history
  • Loading branch information
IZUMI-Zu authored Sep 8, 2024
1 parent 368c967 commit aa7219a
Show file tree
Hide file tree
Showing 7 changed files with 282 additions and 163 deletions.
28 changes: 21 additions & 7 deletions EnterCasdoorSdkConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,13 @@
// See the License for the specific language governing permissions and
// limitations under the License.

import React from "react";
import React, {useState} from "react";
import {ScrollView, Text, View} from "react-native";
import {Button, IconButton, Portal, TextInput} from "react-native-paper";
import Toast from "react-native-toast-message";
import DefaultCasdoorSdkConfig from "./DefaultCasdoorSdkConfig";
import PropTypes from "prop-types";
import ScanQRCodeForLogin from "./ScanLogin";
import useStore from "./useStorage";

const EnterCasdoorSdkConfig = ({onClose, onWebviewClose}) => {
Expand All @@ -39,6 +40,8 @@ const EnterCasdoorSdkConfig = ({onClose, onWebviewClose}) => {
setCasdoorConfig,
} = useStore();

const [showScanner, setShowScanner] = useState(false);

const closeConfigPage = () => {
onClose();
onWebviewClose();
Expand All @@ -58,12 +61,16 @@ const EnterCasdoorSdkConfig = ({onClose, onWebviewClose}) => {
};

const handleScanToLogin = () => {
Toast.show({
type: "info",
text1: "Info",
text2: "Scan to Login functionality not implemented yet.",
autoHide: true,
});
setShowScanner(true);
};

const handleLogin = (loginInfo) => {
setServerUrl(loginInfo.serverUrl);
setClientId(loginInfo.clientId);
setAppName(loginInfo.appName);
setOrganizationName(loginInfo.organizationName);
setShowScanner(false);
onClose();
};

const handleUseDefault = () => {
Expand Down Expand Up @@ -144,6 +151,13 @@ const EnterCasdoorSdkConfig = ({onClose, onWebviewClose}) => {
</Button>
</View>
</ScrollView>
{showScanner && (
<ScanQRCodeForLogin
showScanner={showScanner}
onClose={() => setShowScanner(false)}
onLogin={handleLogin}
/>
)}
</Portal>
);
};
Expand Down
94 changes: 94 additions & 0 deletions QRScanner.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// Copyright 2024 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import React, {useEffect, useState} from "react";
import {Text, View} from "react-native";
import {Button, IconButton, Portal} from "react-native-paper";
import {Camera, CameraView, scanFromURLAsync} from "expo-camera";
import * as ImagePicker from "expo-image-picker";
import PropTypes from "prop-types";

const QRScanner = ({onScan, onClose}) => {
const [hasPermission, setHasPermission] = useState(null);

useEffect(() => {
const getPermissions = async() => {
const {status: cameraStatus} = await Camera.requestCameraPermissionsAsync();
setHasPermission(cameraStatus === "granted");
};

getPermissions();
}, []);

const handleBarCodeScanned = ({type, data}) => {
onScan(type, data);
};

const pickImage = async() => {
const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ImagePicker.MediaTypeOptions.Images,
quality: 1,
});

if (!result.canceled && result.assets[0]) {
const scannedData = await scanFromURLAsync(result.assets[0].uri, ["qr", "pdf417"]);
if (scannedData[0]) {
handleBarCodeScanned({type: scannedData[0].type, data: scannedData[0].data});
}
}
};

if (hasPermission === null) {
return <Text style={{margin: "20%"}}>Requesting permissions...</Text>;
}

if (hasPermission === false) {
return <Text style={{margin: "20%"}}>No access to camera or media library</Text>;
}

return (
<View style={{marginTop: "50%", flex: 1}}>
<Portal>
<CameraView
onBarcodeScanned={handleBarCodeScanned}
barcodeScannerSettings={{
barcodeTypes: ["qr", "pdf417"],
}}
style={{flex: 1}}
/>
<IconButton
icon="close"
size={40}
onPress={onClose}
style={{position: "absolute", top: 30, right: 5}}
/>
<Button
icon="image"
mode="contained"
onPress={pickImage}
style={{position: "absolute", bottom: 20, alignSelf: "center"}}
>
Choose Image
</Button>
</Portal>
</View>
);
};

QRScanner.propTypes = {
onScan: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired,
};

export default QRScanner;
57 changes: 57 additions & 0 deletions ScanLogin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// Copyright 2024 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import React from "react";
import PropTypes from "prop-types";
import QRScanner from "./QRScanner";

const ScanQRCodeForLogin = ({onClose, showScanner, onLogin}) => {
const handleScan = (type, data) => {
if (isValidLoginQR(data)) {
const loginInfo = parseLoginQR(data);
onLogin(loginInfo);
onClose();
}
};

const isValidLoginQR = (data) => {
return data.startsWith("casdoor-app://login/into?");
};

const parseLoginQR = (data) => {
const url = new URL(data);
const params = new URLSearchParams(url.search);

return {
serverUrl: params.get("serverUrl"),
clientId: params.get("clientId"),
appName: params.get("appName"),
organizationName: params.get("organizationName"),
};
};

if (!showScanner) {
return null;
}

return <QRScanner onScan={handleScan} onClose={onClose} />;
};

ScanQRCodeForLogin.propTypes = {
onClose: PropTypes.func.isRequired,
onLogin: PropTypes.func.isRequired,
showScanner: PropTypes.bool.isRequired,
};

export default ScanQRCodeForLogin;
82 changes: 11 additions & 71 deletions ScanQRCode.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,37 +12,15 @@
// See the License for the specific language governing permissions and
// limitations under the License.

import React, {useEffect, useState} from "react";
import {Text, View} from "react-native";
import {Button, IconButton, Portal} from "react-native-paper";
import {Camera, CameraView, scanFromURLAsync} from "expo-camera";
import * as ImagePicker from "expo-image-picker";
import React from "react";
import PropTypes from "prop-types";
import QRScanner from "./QRScanner";
import useProtobufDecoder from "./useProtobufDecoder";

const ScanQRCode = ({onClose, showScanner, onAdd}) => {
ScanQRCode.propTypes = {
onClose: PropTypes.func.isRequired,
onAdd: PropTypes.func.isRequired,
showScanner: PropTypes.bool.isRequired,
};

const [hasPermission, setHasPermission] = useState(null);
const decoder = useProtobufDecoder(require("./google/google_auth.proto"));

useEffect(() => {
const getPermissions = async() => {
const {status: cameraStatus} = await Camera.requestCameraPermissionsAsync();
setHasPermission(cameraStatus === "granted");
// const {status: mediaLibraryStatus} = await ImagePicker.requestMediaLibraryPermissionsAsync();
// setHasMediaLibraryPermission(mediaLibraryStatus === "granted");
};

getPermissions();
}, []);

const handleBarCodeScanned = ({type, data}) => {
// console.log(`Bar code with type ${type} and data ${data} has been scanned!`);
const handleScan = (type, data) => {
const supportedProtocols = ["otpauth", "otpauth-migration"];
const protocolMatch = data.match(new RegExp(`^(${supportedProtocols.join("|")}):`));
if (protocolMatch) {
Expand Down Expand Up @@ -76,55 +54,17 @@ const ScanQRCode = ({onClose, showScanner, onAdd}) => {
onAdd(accounts.map(({accountName, issuer, totpSecret}) => ({accountName, issuer, secretKey: totpSecret})));
};

const pickImage = async() => {
const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ImagePicker.MediaTypeOptions.Images,
quality: 1,
});

if (!result.canceled && result.assets[0]) {
const scannedData = await scanFromURLAsync(result.assets[0].uri, ["qr", "pdf417"]);
if (scannedData[0]) {
handleBarCodeScanned({type: scannedData[0].type, data: scannedData[0].data});
}
}
};

if (hasPermission === null) {
return <Text style={{margin: "20%"}}>Requesting permissions...</Text>;
if (!showScanner) {
return null;
}

if (hasPermission === false) {
return <Text style={{margin: "20%"}}>No access to camera or media library</Text>;
}
return <QRScanner onScan={handleScan} onClose={onClose} />;
};

return (
<View style={{marginTop: "50%", flex: 1}}>
<Portal>
<CameraView
onBarcodeScanned={handleBarCodeScanned}
barcodeScannerSettings={{
barcodeTypes: ["qr", "pdf417"],
}}
style={{flex: 1}}
/>
<IconButton
icon="close"
size={40}
onPress={onClose}
style={{position: "absolute", top: 30, right: 5}}
/>
<Button
icon="image"
mode="contained"
onPress={pickImage}
style={{position: "absolute", bottom: 20, alignSelf: "center"}}
>
Choose Image
</Button>
</Portal>
</View>
);
ScanQRCode.propTypes = {
onClose: PropTypes.func.isRequired,
onAdd: PropTypes.func.isRequired,
showScanner: PropTypes.bool.isRequired,
};

export default ScanQRCode;
5 changes: 2 additions & 3 deletions app.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,13 @@
"icon": "./assets/icon.png",
"userInterfaceStyle": "light",
"sdkVersion": "51.0.0",
"scheme": "casdoor-app",
"splash": {
"image": "./assets/splash.png",
"resizeMode": "contain",
"backgroundColor": "#ffffff"
},
"assetBundlePatterns": [
"**/*"
],
"assetBundlePatterns": ["**/*"],
"ios": {
"supportsTablet": true,
"bundleIdentifier": "org.casdoor.casdoorapp",
Expand Down
Loading

0 comments on commit aa7219a

Please sign in to comment.