Skip to content

Commit

Permalink
Add support for simulating biometric authorization on ios (#236)
Browse files Browse the repository at this point in the history
Description: 
This PR introduces an option to simulate biometric authorization. 
You can simulate: 
- matching face/touch id 
- not matching face/touch id
- biometric authorization enrollment.

Additionally convenient commands were added to vs code to allow for
binding a shortcut to the action of matching or non-matching. The
default keybindings are the same as in xcode.

Relates to : #217


https://github.com/user-attachments/assets/88cc5f2b-fd71-4abd-b302-853578b721de


How was it tested: 

a simple example app is added as a part of this PR that allows testing
introduced functionality .

Future improvments: 
- [ ]  android support

---------

Co-authored-by: filip131311 <[email protected]>
  • Loading branch information
filip131311 and filip131311 authored Aug 29, 2024
1 parent 531c1bd commit f7f0003
Show file tree
Hide file tree
Showing 17 changed files with 2,098 additions and 71 deletions.
22 changes: 22 additions & 0 deletions packages/vscode-extension/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,18 @@
"title": "Diagnostics",
"category": "React Native IDE",
"enablement": "!RNIDE.extensionIsActive"
},
{
"command": "RNIDE.performBiometricAuthorization",
"title": "Perform Biometric Authorization ",
"category": "React Native IDE",
"enablement": "RNIDE.extensionIsActive"
},
{
"command": "RNIDE.performFailedBiometricAuthorization",
"title": "Perform Failed Biometric Authorization",
"category": "React Native IDE",
"enablement": "RNIDE.extensionIsActive"
}
],
"keybindings": [
Expand All @@ -74,6 +86,16 @@
"key": "ctrl+w",
"mac": "cmd+w",
"when": "RNIDE.isTabPanelFocused"
},
{
"command": "RNIDE.performBiometricAuthorization",
"key": "ctrl+shift+M",
"mac": "cmd+shift+M"
},
{
"command": "RNIDE.performFailedBiometricAuthorization",
"key": "ctrl+shift+N",
"mac": "cmd+shift+N"
}
],
"configuration": {
Expand Down
2 changes: 2 additions & 0 deletions packages/vscode-extension/src/common/Project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export type DeviceSettings = {
longitude: number;
isDisabled: boolean;
};
hasEnrolledBiometrics: boolean;
};

export type ProjectState = {
Expand Down Expand Up @@ -111,6 +112,7 @@ export interface ProjectInterface {

getDeviceSettings(): Promise<DeviceSettings>;
updateDeviceSettings(deviceSettings: DeviceSettings): Promise<void>;
sendBiometricAuthorization(match: boolean): Promise<void>;

resumeDebugger(): Promise<void>;
stepOverDebugger(): Promise<void>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,14 @@ import { getAppCachesDir, getNativeABI } from "../utilities/common";
import { ANDROID_HOME } from "../utilities/android";
import { ChildProcess, exec, lineReader } from "../utilities/subprocess";
import { v4 as uuidv4 } from "uuid";
import { AndroidBuildResult, BuildResult } from "../builders/BuildManager";
import { BuildResult } from "../builders/BuildManager";
import { AndroidSystemImageInfo, DeviceInfo, DevicePlatform } from "../common/DeviceManager";
import { Logger } from "../Logger";
import { AppPermissionType, DeviceSettings } from "../common/Project";
import { getAndroidSystemImages } from "../utilities/sdkmanager";
import { EXPO_GO_PACKAGE_NAME, fetchExpoLaunchDeeplink } from "../builders/expoGo";
import { Platform } from "../utilities/platform";
import { AndroidBuildResult } from "../builders/buildAndroid";

export const EMULATOR_BINARY = Platform.select({
macos: path.join(ANDROID_HOME, "emulator", "emulator"),
Expand Down Expand Up @@ -361,6 +362,10 @@ export class AndroidEmulatorDevice extends DeviceBase {
makePreview(): Preview {
return new Preview(["android", this.serial!]);
}

async sendBiometricAuthorization(isMatch: boolean) {
// TO DO: implement android biometric authorization
}
}

export async function createEmulator(displayName: string, systemImage: AndroidSystemImageInfo) {
Expand Down
1 change: 1 addition & 0 deletions packages/vscode-extension/src/devices/DeviceBase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export abstract class DeviceBase implements Disposable {

abstract bootDevice(): Promise<void>;
abstract changeSettings(settings: DeviceSettings): Promise<void>;
abstract sendBiometricAuthorization(isMatch: boolean): Promise<void>;
abstract installApp(build: BuildResult, forceReinstall: boolean): Promise<void>;
abstract launchApp(build: BuildResult, metroPort: number, devtoolsPort: number): Promise<void>;
abstract makePreview(): Preview;
Expand Down
39 changes: 38 additions & 1 deletion packages/vscode-extension/src/devices/IosSimulatorDevice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@ import { Logger } from "../Logger";
import { exec } from "../utilities/subprocess";
import { getAvailableIosRuntimes } from "../utilities/iosRuntimes";
import { IOSDeviceInfo, IOSRuntimeInfo, DevicePlatform, DeviceInfo } from "../common/DeviceManager";
import { BuildResult, IOSBuildResult } from "../builders/BuildManager";
import { BuildResult } from "../builders/BuildManager";
import path from "path";
import fs from "fs";
import { AppPermissionType, DeviceSettings } from "../common/Project";
import { EXPO_GO_BUNDLE_ID, fetchExpoLaunchDeeplink } from "../builders/expoGo";
import { ExecaError } from "execa";
import { IOSBuildResult } from "../builders/buildIOS";

interface SimulatorInfo {
availability?: string;
Expand Down Expand Up @@ -125,6 +126,42 @@ export class IosSimulatorDevice extends DeviceBase {
`${settings.location.latitude.toString()},${settings.location.longitude.toString()}`,
]);
}
await exec("xcrun", [
"simctl",
"--set",
deviceSetLocation,
"spawn",
this.deviceUDID,
"notifyutil",
"-s",
"com.apple.BiometricKit.enrollmentChanged",
settings.hasEnrolledBiometrics ? "1" : "0",
]);
await exec("xcrun", [
"simctl",
"--set",
deviceSetLocation,
"spawn",
this.deviceUDID,
"notifyutil",
"-p",
"com.apple.BiometricKit.enrollmentChanged",
]);
}
async sendBiometricAuthorization(isMatch: boolean) {
const deviceSetLocation = getOrCreateDeviceSet();
await exec("xcrun", [
"simctl",
"--set",
deviceSetLocation,
"spawn",
this.deviceUDID,
"notifyutil",
"-p",
isMatch
? "com.apple.BiometricKit_Sim.fingerTouch.match"
: "com.apple.BiometricKit_Sim.fingerTouch.nomatch",
]);
}

async configureMetroPort(bundleID: string, metroPort: number) {
Expand Down
17 changes: 17 additions & 0 deletions packages/vscode-extension/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,15 @@ export async function activate(context: ExtensionContext) {
{ webviewOptions: { retainContextWhenHidden: true } }
)
);
context.subscriptions.push(
commands.registerCommand("RNIDE.performBiometricAuthorization", performBiometricAuthorization)
);
context.subscriptions.push(
commands.registerCommand(
"RNIDE.performFailedBiometricAuthorization",
performFailedBiometricAuthorization
)
);
context.subscriptions.push(commands.registerCommand("RNIDE.openDevMenu", openDevMenu));
context.subscriptions.push(commands.registerCommand("RNIDE.closePanel", closeIDEPanel));
context.subscriptions.push(commands.registerCommand("RNIDE.openPanel", showIDEPanel));
Expand Down Expand Up @@ -338,6 +347,14 @@ async function openDevMenu() {
Project.currentProject?.openDevMenu();
}

async function performBiometricAuthorization() {
Project.currentProject?.sendBiometricAuthorization(true);
}

async function performFailedBiometricAuthorization() {
Project.currentProject?.sendBiometricAuthorization(false);
}

async function diagnoseWorkspaceStructure() {
const appRootFolder = await configureAppRootFolder();
if (appRootFolder) {
Expand Down
4 changes: 4 additions & 0 deletions packages/vscode-extension/src/project/deviceSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -258,4 +258,8 @@ export class DeviceSession implements Disposable {
public focusBuildOutput() {
this.buildManager.focusBuildOutput();
}

public async sendBiometricAuthorization(isMatch: boolean) {
await this.device.sendBiometricAuthorization(isMatch);
}
}
15 changes: 15 additions & 0 deletions packages/vscode-extension/src/project/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,13 +64,25 @@ export class Project
longitude: 19.965474,
isDisabled: true,
},
hasEnrolledBiometrics: false,
};

constructor(
private readonly deviceManager: DeviceManager,
private readonly dependencyManager: DependencyManager
) {
Project.currentProject = this;
this.deviceSettings = extensionContext.workspaceState.get(DEVICE_SETTINGS_KEY) ?? {
appearance: "dark",
contentSize: "normal",
location: {
latitude: 50.048653,
longitude: 19.965474,
isDisabled: false,
},
hasEnrolledBiometrics: false,
};
this.devtools = new Devtools();
this.metro = new Metro(this.devtools, this);
this.start(false, false);
this.trySelectingInitialDevice();
Expand Down Expand Up @@ -404,6 +416,9 @@ export class Project
await this.deviceSession?.changeDeviceSettings(settings);
this.eventEmitter.emit("deviceSettingsChanged", this.deviceSettings);
}
public async sendBiometricAuthorization(isMatch: boolean) {
await this.deviceSession?.sendBiometricAuthorization(isMatch);
}

private reportStageProgress(stageProgress: number, stage: string) {
if (stage !== this.projectState.startupMessage) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import { DeviceLocationView } from "../views/DeviceLocationView";
import { JSX } from "react/jsx-runtime";
import DiagnosticView from "../views/DiagnosticView";
import { useModal } from "../providers/ModalProvider";
import { DevicePlatform } from "../../common/DeviceManager";
import { KeybindingInfo } from "./shared/KeybindingInfo";

const contentSizes = [
"xsmall",
Expand Down Expand Up @@ -117,7 +119,8 @@ function DeviceSettingsDropdown({ children, disabled }: DeviceSettingsDropdownPr
</Slider.Root>
<span className="device-settings-large-text-indicator" />
</div>

<div className="device-settings-margin" />
{projectState.selectedDevice?.platform === DevicePlatform.IOS && <BiometricsItem />}
<DropdownMenu.Arrow className="dropdown-menu-arrow" />
</form>
<Label>Device Location</Label>
Expand Down Expand Up @@ -158,4 +161,59 @@ function DeviceSettingsDropdown({ children, disabled }: DeviceSettingsDropdownPr
);
}

const BiometricsItem = () => {
const { project, deviceSettings } = useProject();

return (
<DropdownMenu.Sub>
<DropdownMenu.SubTrigger className="dropdown-menu-item">
<span className="codicon codicon-layout" />
Biometrics
<span className="codicon codicon-chevron-right right-slot" />
</DropdownMenu.SubTrigger>

<DropdownMenu.Portal>
<DropdownMenu.SubContent className="dropdown-menu-content">
<DropdownMenu.Item
className="dropdown-menu-item"
onSelect={() => {
project.updateDeviceSettings({
...deviceSettings,
hasEnrolledBiometrics: !deviceSettings.hasEnrolledBiometrics,
});
}}>
<span className="codicon codicon-layout-sidebar-left" />
Enrolled
{deviceSettings.hasEnrolledBiometrics && (
<span className="codicon codicon-check right-slot" />
)}
</DropdownMenu.Item>
<DropdownMenu.Item
className="dropdown-menu-item"
onSelect={() => {
project.sendBiometricAuthorization(true);
}}>
<span className="codicon codicon-layout-sidebar-left" />
<div className="dropdown-menu-item-content">
Matching ID
<KeybindingInfo commandName="RNIDE.performBiometricAuthorization" />
</div>
</DropdownMenu.Item>
<DropdownMenu.Item
className="dropdown-menu-item"
onSelect={() => {
project.sendBiometricAuthorization(false);
}}>
<span className="codicon codicon-layout-sidebar-left" />
<div className="dropdown-menu-item-content">
Non-Matching ID
<KeybindingInfo commandName="RNIDE.performFailedBiometricAuthorization" />
</div>
</DropdownMenu.Item>
</DropdownMenu.SubContent>
</DropdownMenu.Portal>
</DropdownMenu.Sub>
);
};

export default DeviceSettingsDropdown;
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const ProjectContext = createContext<ProjectContextProps>({
deviceSettings: {
appearance: "dark",
contentSize: "normal",
hasEnrolledBiometrics: false,
location: {
latitude: 50.048653,
longitude: 19.965474,
Expand All @@ -39,6 +40,7 @@ export default function ProjectProvider({ children }: PropsWithChildren) {
const [deviceSettings, setDeviceSettings] = useState<DeviceSettings>({
appearance: "dark",
contentSize: "normal",
hasEnrolledBiometrics: false,
location: {
latitude: 50.048653,
longitude: 19.965474,
Expand Down
45 changes: 44 additions & 1 deletion test-apps/react-native-75/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@
* @format
*/

import React from 'react';
import React, { useState } from 'react';
import type {PropsWithChildren} from 'react';
import {
Button,
SafeAreaView,
ScrollView,
StatusBar,
Expand All @@ -16,6 +17,7 @@ import {
useColorScheme,
View,
} from 'react-native';
import ReactNativeBiometrics from 'react-native-biometrics';

import {
Colors,
Expand Down Expand Up @@ -56,6 +58,29 @@ function Section({children, title}: SectionProps): React.JSX.Element {
}

function App(): React.JSX.Element {
const [isAuthorized, setIsAuthorized] = useState(false);
const [biometrics, setBiometrics] = useState(new ReactNativeBiometrics({allowDeviceCredentials: true}) );
async function AuthSimplePrompt(){
const { biometryType } = await biometrics.isSensorAvailable();
console.log("isSensorAvailable", biometryType)
try{
const {success} = await biometrics.simplePrompt({
promptMessage: 'Confirmation',
});

if (success) {
setIsAuthorized(true);
}
else{
setIsAuthorized(false);
}
}catch(e){
setIsAuthorized(false);
console.log("isSensorAvailable", e);
}

}

const isDarkMode = useColorScheme() === 'dark';

const backgroundStyle = {
Expand All @@ -72,6 +97,24 @@ function App(): React.JSX.Element {
contentInsetAdjustmentBehavior="automatic"
style={backgroundStyle}>
<Header />
<View
style={{
display: "flex",
justifyContent:"center",
alignItems: "center",
height: 500,
backgroundColor: isDarkMode ? Colors.black : Colors.white,
}}>
<Button title = {"Try and Authorize"} onPress={AuthSimplePrompt}></Button>
<View>
{isAuthorized &&
<Text>I am authorized!</Text>
}
{!isAuthorized &&
<Text>I am NOT authorized!</Text>
}
</View>
</View>
<View
style={{
backgroundColor: isDarkMode ? Colors.black : Colors.white,
Expand Down
Loading

0 comments on commit f7f0003

Please sign in to comment.