add zkey download on iOS and android

- cleanups too, especially ProveScreen
This commit is contained in:
0xturboblitz
2024-04-03 20:19:45 -07:00
parent f9a21f53ec
commit 38fd190c75
15 changed files with 186 additions and 173 deletions

View File

@@ -23,10 +23,14 @@ import { toastConfig } from './src/utils/toastConfig';
import { Buffer } from 'buffer';
import { YStack } from 'tamagui';
import { prove } from './src/utils/prover';
import RNFS from 'react-native-fs';
import { ARKZKEY_URL, ZKEY_URL } from '../common/src/constants/constants';
global.Buffer = Buffer;
console.log('DEFAULT_PNUMBER', DEFAULT_PNUMBER);
const localZkeyPath = RNFS.DocumentDirectoryPath + '/proof_of_passport.zkey';
function App(): JSX.Element {
const [passportNumber, setPassportNumber] = useState(DEFAULT_PNUMBER ?? "");
const [dateOfBirth, setDateOfBirth] = useState(DEFAULT_DOB ?? '');
@@ -39,6 +43,7 @@ function App(): JSX.Element {
const [proof, setProof] = useState<{ proof: string, inputs: string } | null>(null);
const [mintText, setMintText] = useState<string>("");
const [majority, setMajority] = useState<number>(18);
const [zkeydownloadStatus, setDownloadStatus] = useState<"not_started" | "downloading" | "completed" | "error">("not_started");
const [disclosure, setDisclosure] = useState({
issuing_state: false,
@@ -70,17 +75,57 @@ function App(): JSX.Element {
}, []);
useEffect(() => {
initMopro()
downloadZkey()
}, []);
async function downloadZkey() {
console.log('launching zkey download')
async function initMopro() {
if (Platform.OS === 'android') {
const res = await NativeModules.Prover.runInitAction()
console.log('init done')
console.log('init res', res)
console.log('Mopro init res:', res)
}
}
async function downloadZkey() {
console.log('launching zkey download')
// temporarily, as arkzkey is still bundled in mopro
if (Platform.OS === 'android') {
setDownloadStatus('completed');
return;
}
const fileExists = await RNFS.exists(localZkeyPath);
if (!fileExists) {
setDownloadStatus('downloading');
const options = {
// @ts-ignore
fromUrl: Platform.OS === 'android' ? ARKZKEY_URL : ZKEY_URL,
toFile: localZkeyPath,
background: true,
begin: () => {
console.log('Download has begun');
},
progress: (res: any) => {
console.log((res.bytesWritten / res.contentLength * 100).toFixed(2) + '%');
},
};
RNFS.downloadFile(options).promise
.then(() => setDownloadStatus('completed'))
.catch((error) => {
console.error(error);
setDownloadStatus('error');
Toast.show({
type: 'error',
text1: `Error: ${error.message}`,
position: 'top',
bottomOffset: 80,
})
});
} else {
// const res = await NativeModules.Prover.downloadZkey("url")
setDownloadStatus('completed');
}
}
@@ -104,7 +149,7 @@ function App(): JSX.Element {
});
};
const handleProve = (path: string) => {
const handleProve = () => {
prove({
passportData,
disclosure,
@@ -113,7 +158,7 @@ function App(): JSX.Element {
setGeneratingProof,
setProofTime,
setProof,
}, path);
});
};
const handleMint = () => {
@@ -151,6 +196,7 @@ function App(): JSX.Element {
setDateOfExpiry={setDateOfExpiry}
majority={majority}
setMajority={setMajority}
zkeydownloadStatus={zkeydownloadStatus}
/>
</YStack>
<Toast config={toastConfig} />

View File

@@ -120,8 +120,8 @@ dependencies {
// The version of react-native is set by the React Native Gradle Plugin
implementation("com.facebook.react:react-android")
implementation(project(":react-native-passport-reader")) {
exclude group: 'edu.ucar', module: 'jj2000'
}
exclude group: 'edu.ucar', module: 'jj2000'
}
implementation project(':passportreader')
implementation 'org.jmrtd:jmrtd:0.7.18'
@@ -139,7 +139,7 @@ dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation("net.java.dev.jna:jna:5.13.0@aar")
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android'
implementation project(':react-native-fs')
}
apply from: file("../../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesAppBuildGradle(project)

View File

@@ -8,11 +8,11 @@ import com.facebook.react.ReactPackage;
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint;
import com.facebook.react.defaults.DefaultReactNativeHost;
import com.facebook.soloader.SoLoader;
import com.proofofpassport.CameraActivityPackage; // import new package for mrz reading
import com.proofofpassport.CameraActivityPackage;
import io.tradle.nfc.RNPassportReaderPackage;
import java.util.List;
import com.proofofpassport.prover.ProverPackage;
import com.rnfs.RNFSPackage;
public class MainApplication extends Application implements ReactApplication {

View File

@@ -45,36 +45,6 @@ class ProverModule(reactContext: ReactApplicationContext) : ReactContextBaseJava
}
}
// @ReactMethod
// fun downloadFile(url: String, fileName: String, promise: Promise) {
// val client = OkHttpClient()
// val request = Request.Builder().url(url).build()
// try {
// client.newCall(request).execute().use { response ->
// if (!response.isSuccessful) throw IOException("Failed to download file: $response")
// // Use the app's internal files directory
// val fileOutputStream = reactContext.openFileOutput(fileName, Context.MODE_PRIVATE)
// val inputStream = response.body?.byteStream()
// inputStream.use { input ->
// fileOutputStream.use { output ->
// input?.copyTo(output)
// }
// }
// // Resolve the promise with the file path
// val file = File(reactContext.filesDir, fileName)
// promise.resolve(file.absolutePath)
// }
// } catch (e: Exception) {
// // Reject the promise if an exception occurs
// promise.reject(e)
// }
// }
@ReactMethod
// fun runProveAction(inputs: ReadableMap, zkeypath: String, promise: Promise) {
fun runProveAction(inputs: ReadableMap, promise: Promise) {

View File

@@ -6,3 +6,5 @@ include ':react-native-passport-reader'
project(':react-native-passport-reader').projectDir = new File(rootProject.projectDir, './react-native-passport-reader/android')
include ':passportreader'
project(':passportreader').projectDir = new File(rootProject.projectDir, './android-passport-reader/app')
include ':react-native-fs'
project(':react-native-fs').projectDir = new File(settingsDir, '../node_modules/react-native-fs/android')

View File

@@ -40,6 +40,7 @@ target 'ProofOfPassport' do
use_frameworks!
pod 'NFCPassportReader', git: 'https://github.com/0xturboblitz/NFCPassportReader.git', commit: '310ecb519655d9ed8b1afc5eb490b2f51a4d3595'
pod 'QKMRZScanner'
pod 'RNFS', :path => '../node_modules/react-native-fs'
use_react_native!(
:path => config[:reactNativePath],

View File

@@ -396,6 +396,8 @@ PODS:
- React-perflogger (= 0.72.3)
- RNCClipboard (1.5.1):
- React-Core
- RNFS (2.20.0):
- React-Core
- RNSVG (13.4.0):
- React-Core
- SocketRocket (0.6.1)
@@ -444,6 +446,7 @@ DEPENDENCIES:
- React-utils (from `../node_modules/react-native/ReactCommon/react/utils`)
- ReactCommon/turbomodule/core (from `../node_modules/react-native/ReactCommon`)
- "RNCClipboard (from `../node_modules/@react-native-community/clipboard`)"
- RNFS (from `../node_modules/react-native-fs`)
- RNSVG (from `../node_modules/react-native-svg`)
- Yoga (from `../node_modules/react-native/ReactCommon/yoga`)
@@ -536,6 +539,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native/ReactCommon"
RNCClipboard:
:path: "../node_modules/@react-native-community/clipboard"
RNFS:
:path: "../node_modules/react-native-fs"
RNSVG:
:path: "../node_modules/react-native-svg"
Yoga:
@@ -590,11 +595,12 @@ SPEC CHECKSUMS:
React-utils: d55ba834beb39f01b0b470ae43478c0a3a024abe
ReactCommon: 68e3a815fbb69af3bb4196e04c6ae7abb306e7a8
RNCClipboard: 41d8d918092ae8e676f18adada19104fa3e68495
RNFS: 4ac0f0ea233904cb798630b3c077808c06931688
RNSVG: 07dbd870b0dcdecc99b3a202fa37c8ca163caec2
SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17
SwiftyTesseract: 1f3d96668ae92dc2208d9842c8a59bea9fad2cbb
Yoga: 8796b55dba14d7004f980b54bcc9833ee45b28ce
PODFILE CHECKSUM: 2378ca5cd60629f1338780ea1a28f12a2a0d2d58
PODFILE CHECKSUM: a6500379bd8743894089afac29ed92bb8a67415a
COCOAPODS: 1.14.3

View File

@@ -33,6 +33,7 @@
"react": "18.2.0",
"react-native": "0.72.3",
"react-native-canvas": "^0.1.39",
"react-native-fs": "^2.20.0",
"react-native-passport-reader": "^1.0.3",
"react-native-svg": "13.4.0",
"react-native-toast-message": "^2.2.0",

View File

@@ -22,7 +22,7 @@ interface MainScreenProps {
address: string;
setAddress: (address: string) => void;
generatingProof: boolean;
handleProve: (path: string) => void;
handleProve: () => void;
step: number;
mintText: string;
proof: any;
@@ -37,6 +37,7 @@ interface MainScreenProps {
setDateOfExpiry: (date: string) => void;
majority: number;
setMajority: (age: number) => void;
zkeydownloadStatus: string;
}
const MainScreen: React.FC<MainScreenProps> = ({
@@ -62,7 +63,8 @@ const MainScreen: React.FC<MainScreenProps> = ({
dateOfExpiry,
setDateOfExpiry,
majority,
setMajority
setMajority,
zkeydownloadStatus
}) => {
const [NFCScanIsOpen, setNFCScanIsOpen] = useState(false);
const [SettingsIsOpen, setSettingsIsOpen] = useState(false);
@@ -374,7 +376,9 @@ const MainScreen: React.FC<MainScreenProps> = ({
ens={ens}
setEns={setEns}
majority={majority}
setMajority={setMajority} />
setMajority={setMajority}
zkeydownloadStatus={zkeydownloadStatus}
/>
</Tabs.Content>
<Separator />
{(!keyboardVisible || Platform.OS == "ios") &&

View File

@@ -1,21 +1,16 @@
import React, { useState, useEffect } from 'react';
import { NativeModules } from 'react-native';
import { YStack, XStack, Text, Checkbox, Input, Button, Spinner, Image, useWindowDimensions, ButtonText, } from 'tamagui';
import { YStack, XStack, Text, Checkbox, Input, Button, Spinner, Image, useWindowDimensions } from 'tamagui';
import { Check, LayoutGrid, Scan, Copy, Plus, Minus } from '@tamagui/lucide-icons';
import { getFirstName, formatDuration } from '../../utils/utils';
import { getFirstName, formatDuration, maskString, shortenInput, getTx } from '../../utils/utils';
import { attributeToPosition } from '../../../common/src/constants/constants';
import { Steps } from '../utils/utils';
import USER from '../images/user.png'
import ProofGrid from '../components/ProofGrid';
import { App } from '../utils/AppClass';
import { Keyboard, Platform } from 'react-native';
import { DEFAULT_ADDRESS } from '@env';
import Clipboard from '@react-native-community/clipboard';
import Toast from 'react-native-toast-message';
const { ethers } = require('ethers');
const fileName = "passport.arkzkey"
const path = "/data/user/0/com.proofofpassport/files/" + fileName
import { ethers } from 'ethers';
interface ProveScreenProps {
selectedApp: App | null;
@@ -25,7 +20,7 @@ interface ProveScreenProps {
address: string;
setAddress: (address: string) => void;
generatingProof: boolean;
handleProve: (path: string) => void;
handleProve: () => void;
handleMint: () => void;
step: number;
mintText: string;
@@ -36,6 +31,7 @@ interface ProveScreenProps {
setEns: (ens: string) => void;
majority: number;
setMajority: (age: number) => void;
zkeydownloadStatus: string;
}
const ProveScreen: React.FC<ProveScreenProps> = ({
@@ -56,28 +52,12 @@ const ProveScreen: React.FC<ProveScreenProps> = ({
ens,
setEns,
majority,
setMajority
setMajority,
zkeydownloadStatus
}) => {
const [zkeyLoading, setZkeyLoading] = useState(false);
const [zkeyLoaded, setZkeyLoaded] = useState(true);
const [age, setAge] = useState(18);
const incrementAge = () => setAge(prevAge => prevAge + 1);
const decrementAge = () => setAge(prevAge => prevAge > 0 ? prevAge - 1 : 0);
const getTx = (input: string | null): string => {
if (!input) return '';
const transaction = input.split(' ').filter(word => word.startsWith('0x')).join(' ');
return transaction;
}
const shortenInput = (input: string | null): string => {
if (!input) return '';
if (input.length > 9) {
return input.substring(0, 25) + '\u2026';
} else {
return input;
}
}
const { height } = useWindowDimensions();
const [inputValue, setInputValue] = useState(DEFAULT_ADDRESS ?? '');
const provider = new ethers.JsonRpcProvider(`https://eth-mainnet.g.alchemy.com/v2/lpOn3k6Fezetn1e5QF-iEsn-J0C6oGE0`);
const copyToClipboard = (input: string) => {
Clipboard.setString(input);
@@ -89,46 +69,6 @@ const ProveScreen: React.FC<ProveScreenProps> = ({
})
};
const downloadZkey = async () => {
// TODO: don't redownload if already in the file system at path, if downloaded from previous session
setZkeyLoading(true);
// Allow the spinner to show up before app freeze on android
await new Promise(resolve => setTimeout(resolve, 1500));
try {
console.log('Downloading file...')
const result = await NativeModules.RNPassportReader.downloadFile(
'https://current-pop-zkey.s3.eu-north-1.amazonaws.com/proof_of_passport_final_merkle_and_age.arkzkey',
fileName
);
console.log("Download successful");
console.log(result);
setZkeyLoaded(true);
setZkeyLoading(false);
} catch (e: any) {
console.log("Download not successful");
Toast.show({
type: 'error',
text1: `Error: ${e.message}`,
position: 'top',
bottomOffset: 80,
})
setZkeyLoading(false);
}
};
const maskString = (input: string): string => {
if (input.length <= 5) {
return input.charAt(0) + '*'.repeat(input.length - 1);
} else {
return input.charAt(0) + input.charAt(1) + '*'.repeat(input.length - 2);
}
}
const [inputValue, setInputValue] = useState(DEFAULT_ADDRESS ?? '');
const provider = new ethers.JsonRpcProvider(`https://eth-mainnet.g.alchemy.com/v2/lpOn3k6Fezetn1e5QF-iEsn-J0C6oGE0`);
useEffect(() => {
if (ens != '' && inputValue == '') {
setInputValue(ens);
@@ -137,7 +77,6 @@ const ProveScreen: React.FC<ProveScreenProps> = ({
else if (address != ethers.ZeroAddress && inputValue == '') {
setInputValue(address);
}
}, [])
useEffect(() => {
@@ -164,10 +103,6 @@ const ProveScreen: React.FC<ProveScreenProps> = ({
})
if (hideData) {
console.log(maskString(address));
// setInputValue(maskString(address));
}
else {
// setInputValue(address);
}
} else {
Toast.show({
@@ -196,26 +131,6 @@ const ProveScreen: React.FC<ProveScreenProps> = ({
resolveENS();
}, [inputValue]);
// Keyboard management
const [keyboardVisible, setKeyboardVisible] = useState(false);
const { height, width } = useWindowDimensions();
useEffect(() => {
const showSubscription = Keyboard.addListener('keyboardDidShow', () => {
setKeyboardVisible(true);
});
const hideSubscription = Keyboard.addListener('keyboardDidHide', () => {
setKeyboardVisible(false);
});
return () => {
showSubscription.remove();
hideSubscription.remove();
};
}, []);
return (
<YStack px="$4" f={1} >
{(step >= Steps.NFC_SCAN_COMPLETED && selectedApp != null) ?
@@ -239,7 +154,6 @@ const ProveScreen: React.FC<ProveScreenProps> = ({
uri: passportData.photoBase64 ?? USER,
}}
/>
}
</YStack>
<Text fontSize="$5" fontWeight="bold">Hi {hideData ? maskString(getFirstName(passportData.mrz)) : getFirstName(passportData.mrz)} 👋</Text>
@@ -296,25 +210,52 @@ const ProveScreen: React.FC<ProveScreenProps> = ({
})}
</YStack>
</YStack>
<Button disabled={zkeyLoading || (address == ethers.ZeroAddress)} borderRadius={100} onPress={() => { (!zkeyLoaded && Platform.OS != "ios") ? downloadZkey() : handleProve(path) }} mt="$8" backgroundColor={address == ethers.ZeroAddress ? "#cecece" : "#3185FC"} alignSelf='center' >
{!zkeyLoaded && Platform.OS != "ios" ? (
<XStack ai="center" gap="$2">
{zkeyLoading && <Spinner />}
<Text color="white" fow="bold">{zkeyLoading ? "Downloading ZK circuit" : "Download ZK circuit"}</Text>
<Button
disabled={zkeydownloadStatus != "completed" || (address == ethers.ZeroAddress)}
borderRadius={100}
onPress={handleProve}
mt="$8"
backgroundColor={address == ethers.ZeroAddress ? "#cecece" : "#3185FC"}
alignSelf='center'
>
{zkeydownloadStatus === "downloading" ? (
<XStack ai="center" gap="$1">
<Spinner />
<Text color="white" fow="bold">
Downloading ZK proving key
</Text>
</XStack>
) : zkeydownloadStatus === "error" ? (
<XStack ai="center" gap="$1">
<Spinner />
<Text color="white" fow="bold">
Error downloading ZK proving key
</Text>
</XStack>
) : generatingProof ? (
<XStack ai="center" gap="$1">
<Spinner />
<Text color="white" marginLeft="$2" fow="bold" >Generating ZK proof</Text>
<Text color="white" marginLeft="$2" fow="bold">
Generating ZK proof
</Text>
</XStack>
) : (
<Text color="white" fow="bold">Generate ZK proof</Text>
<Text color="white" fow="bold">
Generate ZK proof
</Text>
)}
</Button>
{(height > 750) && <Text fontSize={10} color={generatingProof ? "gray" : "white"} pb="$2" alignSelf='center'>This operation can take up to 2 mn, phone may freeze during this time</Text>}
</YStack >
{
(height > 750) && <Text
fontSize={10}
color={generatingProof ? "gray" : "white"}
pb="$2"
alignSelf='center'
>
This operation can take up to 2 mn, phone may freeze during this time
</Text>
}
</YStack>
) : step === Steps.TX_MINTED ? (
<YStack flex={1} justifyContent='center' alignItems='center' gap="$5">
<XStack flex={1} />
@@ -357,23 +298,19 @@ const ProveScreen: React.FC<ProveScreenProps> = ({
</XStack>
: <Text color="white" fow="bold" >{selectedApp?.mintphrase}</Text>}
</Button>
</YStack>
)
) :
(
<YStack flex={1} justifyContent='center' alignItems='center'>
<Text fontSize={17} textAlign='center' fow="bold">Please scan your passport and select an app to generate ZK proof</Text>
<XStack mt="$8" gap="$7">
<Scan size="$4" color={step < Steps.NFC_SCAN_COMPLETED ? "black" : "#3185FC"} />
<LayoutGrid size="$4" color={selectedApp == null ? "black" : "#3185FC"} />
</XStack>
</YStack>
)
}
) : (
<YStack flex={1} justifyContent='center' alignItems='center'>
<Text fontSize={17} textAlign='center' fow="bold">Please scan your passport and select an app to generate ZK proof</Text>
<XStack mt="$8" gap="$7">
<Scan size="$4" color={step < Steps.NFC_SCAN_COMPLETED ? "black" : "#3185FC"} />
<LayoutGrid size="$4" color={selectedApp == null ? "black" : "#3185FC"} />
</XStack>
</YStack>
)}
</YStack >
);
};
export default ProveScreen;
export default ProveScreen;

View File

@@ -75,6 +75,8 @@ export const mint = async ({ proof, setStep, setMintText }: MinterProps) => {
}
} catch (err: any) {
console.log('err', err);
setStep(Steps.PROOF_GENERATED);
setMintText(`Error minting SBT. Network: Sepolia.`);
if (err.isAxiosError && err.response) {
const errorMessage = err.response.data.error;
console.log('Server error message:', errorMessage);
@@ -99,6 +101,5 @@ export const mint = async ({ proof, setStep, setMintText }: MinterProps) => {
console.log('Failed to parse blockchain error');
}
}
setMintText(`Error minting SBT. Network: Sepolia.`);
}
};

View File

@@ -24,7 +24,7 @@ export const prove = async ({
setGeneratingProof,
setProofTime,
setProof,
}: ProverProps, path?: string) => {
}: ProverProps) => {
if (passportData === null) {
console.log('passport data is null');
return;
@@ -57,7 +57,7 @@ export const prove = async ({
});
const start = Date.now();
await generateProof(inputs, setProofTime, setProof, setGeneratingProof, setStep, path);
await generateProof(inputs, setProofTime, setProof, setGeneratingProof, setStep);
const end = Date.now();
console.log('Total proof time from frontend:', end - start);
} catch (error: any) {
@@ -77,13 +77,14 @@ const generateProof = async (
setProof: (value: { proof: string; inputs: string } | null) => void,
setGeneratingProof: (value: boolean) => void,
setStep: (value: number) => void,
path?: string,
) => {
try {
console.log('launching generateProof function');
console.log('inputs in App.tsx', inputs);
// await NativeModules.Prover.runInitAction();
if (Platform.OS == "android") {
await NativeModules.Prover.runInitAction();
}
console.log('running prove action');
const startTime = Date.now();

View File

@@ -38,3 +38,26 @@ export function checkInputs(
message: ''
};
}
export const maskString = (input: string): string => {
if (input.length <= 5) {
return input.charAt(0) + '*'.repeat(input.length - 1);
} else {
return input.charAt(0) + input.charAt(1) + '*'.repeat(input.length - 2);
}
}
export const getTx = (input: string | null): string => {
if (!input) return '';
const transaction = input.split(' ').filter(word => word.startsWith('0x')).join(' ');
return transaction;
}
export const shortenInput = (input: string | null): string => {
if (!input) return '';
if (input.length > 9) {
return input.substring(0, 25) + '\u2026';
} else {
return input;
}
}

View File

@@ -3615,6 +3615,11 @@ balanced-match@^1.0.0:
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
base-64@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/base-64/-/base-64-0.1.0.tgz#780a99c84e7d600260361511c4877613bf24f6bb"
integrity sha512-Y5gU45svrR5tI2Vt/X9GPd3L0HNIKzGu202EjxrXMpuc2V2CiKgemAbUUsqYmZJvPtCXoUKjNZwBJzsNScUbXA==
base64-js@^1.1.2, base64-js@^1.3.1:
version "1.5.1"
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
@@ -7489,6 +7494,14 @@ react-native-dotenv@^3.4.9:
dependencies:
dotenv "^16.4.1"
react-native-fs@^2.20.0:
version "2.20.0"
resolved "https://registry.yarnpkg.com/react-native-fs/-/react-native-fs-2.20.0.tgz#05a9362b473bfc0910772c0acbb73a78dbc810f6"
integrity sha512-VkTBzs7fIDUiy/XajOSNk0XazFE9l+QlMAce7lGuebZcag5CnjszB+u4BdqzwaQOdcYb5wsJIsqq4kxInIRpJQ==
dependencies:
base-64 "^0.1.0"
utf8 "^3.0.0"
react-native-passport-reader@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/react-native-passport-reader/-/react-native-passport-reader-1.0.3.tgz#3242bbdb3c1ade4c050a8632cca6f11fe0edc648"
@@ -8562,6 +8575,11 @@ use-sync-external-store@1.2.0, use-sync-external-store@^1.0.0:
resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a"
integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==
utf8@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/utf8/-/utf8-3.0.0.tgz#f052eed1364d696e769ef058b183df88c87f69d1"
integrity sha512-E8VjFIQ/TyQgp+TZfS6l8yp/xWppSAHzidGiRrqe4bK4XP9pTRyKFgGJpO3SN7zdX4DeomTrwaseCHovfpFcqQ==
util-deprecate@^1.0.1, util-deprecate@~1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"