diff --git a/android/app/src/main/java/com/friendlyplans/MainApplication.java b/android/app/src/main/java/com/friendlyplans/MainApplication.java index c5813f9..69dd269 100644 --- a/android/app/src/main/java/com/friendlyplans/MainApplication.java +++ b/android/app/src/main/java/com/friendlyplans/MainApplication.java @@ -13,6 +13,7 @@ import io.invertase.firebase.fabric.crashlytics.RNFirebaseCrashlyticsPackage; import io.invertase.firebase.analytics.RNFirebaseAnalyticsPackage; import io.invertase.firebase.firestore.RNFirebaseFirestorePackage; +import io.invertase.firebase.storage.RNFirebaseStoragePackage; import java.util.List; @@ -32,6 +33,7 @@ protected List getPackages() { packages.add(new RNFirebaseCrashlyticsPackage()); packages.add(new RNFirebaseAnalyticsPackage()); packages.add(new RNFirebaseFirestorePackage()); + packages.add(new RNFirebaseStoragePackage()); return packages; } diff --git a/ios/FriendlyPlans.xcodeproj/project.pbxproj b/ios/FriendlyPlans.xcodeproj/project.pbxproj index 1613d63..3027824 100644 --- a/ios/FriendlyPlans.xcodeproj/project.pbxproj +++ b/ios/FriendlyPlans.xcodeproj/project.pbxproj @@ -5,7 +5,6 @@ }; objectVersion = 46; objects = { - /* Begin PBXBuildFile section */ 13B07FBC1A68108700A75B9A /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB01A68108700A75B9A /* AppDelegate.m */; }; 13B07FBD1A68108700A75B9A /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB11A68108700A75B9A /* LaunchScreen.xib */; }; diff --git a/package.json b/package.json index 26d7650..c27bf9a 100644 --- a/package.json +++ b/package.json @@ -19,10 +19,14 @@ }, "dependencies": { "@react-native-community/slider": "^2.0.8", + "@react-native-firebase/app": "^6.4.0", + "@react-native-firebase/storage": "^6.4.0", "@types/lodash.every": "^4.6.6", "@types/lodash.isempty": "^4.4.6", "@types/lodash.noop": "^3.0.6", "@types/lodash.sortby": "^4.7.6", + "@types/react-native-uuid": "^1.4.0", + "buffer": "^5.5.0", "formik": "^1.5.7", "i18next": "^18.0.0", "lodash.every": "^4.6.0", @@ -40,10 +44,12 @@ "react-native-floating-action": "^1.19.1", "react-native-gesture-handler": "^1.5.6", "react-native-image-crop-picker": "^0.26.2", + "react-native-image-picker": "^2.3.1", "react-native-localize": "^1.3.1", "react-native-reanimated": "^1.7.0", "react-native-splash-screen": "^3.2.0", "react-native-svg": "^9.13.3", + "react-native-uuid": "^1.4.9", "react-native-vector-icons": "^6.6.0", "react-navigation": "^3.11.0", "yup": "^0.27.0" diff --git a/src/components/runPlanSlide/SlideImage.tsx b/src/components/runPlanSlide/SlideImage.tsx new file mode 100644 index 0000000..c77e4ba --- /dev/null +++ b/src/components/runPlanSlide/SlideImage.tsx @@ -0,0 +1,38 @@ +import React from 'react'; +import { Image, StyleSheet, View } from 'react-native'; +import { palette } from '../../styles'; +import { StyledText } from '../StyledText'; + +interface Props { + imageUri: string; +} + +export class SlideImage extends React.PureComponent { + render() { + if (this.props.imageUri) { + return ( + + + + ); + } + return Loading...; + } +} + +const styles = StyleSheet.create({ + imageContainer: { + flex: 1, + alignSelf: 'stretch', + marginBottom: 16, + }, + loadingText: { + fontSize: 24, + }, + image: { + flex: 1, + }, + nameTextColor: { + color: palette.textBlack, + }, +}); diff --git a/src/infrastructure/Images.ts b/src/infrastructure/Images.ts new file mode 100644 index 0000000..bfc7e62 --- /dev/null +++ b/src/infrastructure/Images.ts @@ -0,0 +1,19 @@ +import uuid from 'react-native-uuid'; +import { getImagesStorage } from '../models/FirebaseRefProxy'; + +export const uploadImage = async (imageUri: string, fileName: string | undefined): Promise => { + const DEFAULT_EXTENSION = '.jpg'; + const fileExtension = fileName ? fileName.split('.').pop() : DEFAULT_EXTENSION; + const uploadedFileName = `${uuid.v1()}.${fileExtension}`; + + await getImagesStorage() + .child(uploadedFileName) + .putFile(imageUri); + return uploadedFileName; +}; + +export const loadImage = async (imageName: string): Promise => { + return getImagesStorage() + .child(imageName) + .getDownloadURL(); +}; diff --git a/src/models/FirebaseRefProxy.ts b/src/models/FirebaseRefProxy.ts index 46973e0..451bb27 100644 --- a/src/models/FirebaseRefProxy.ts +++ b/src/models/FirebaseRefProxy.ts @@ -34,6 +34,13 @@ export const getStudentRef = (studentId: string): RNFirebase.firestore.DocumentR export const getStudentsRef = (userId: string = getAuthenticatedUserId()): RNFirebase.firestore.CollectionReference => getUserRef(userId).collection('students'); +export const getImagesStorage = (userId: string = getAuthenticatedUserId()): RNFirebase.storage.Reference => + firebase + .storage() + .ref() + .child('images') + .child(userId); + export const getUserRef = (userId: string): RNFirebase.firestore.DocumentReference => getUsersRef().doc(userId); const getUsersRef = (): RNFirebase.firestore.CollectionReference => { diff --git a/src/models/PlanElement.ts b/src/models/PlanElement.ts index 07c0e99..c34428b 100644 --- a/src/models/PlanElement.ts +++ b/src/models/PlanElement.ts @@ -7,6 +7,7 @@ export interface PlanElement { completed: boolean; time: number; lector: boolean; + image: string; complete: () => void; update: (changes: any) => void; diff --git a/src/models/PlanItem.tsx b/src/models/PlanItem.tsx index d0de0ea..f61df97 100644 --- a/src/models/PlanItem.tsx +++ b/src/models/PlanItem.tsx @@ -43,6 +43,7 @@ export class PlanItem implements SubscribableModel, PlanElement { plan: Plan, type: PlanItemType, name: string = i18n.t('updatePlan:planItemNamePlaceholder'), + image: string = '', lastItemOrder: number, ): Promise { const { id } = await getPlanItemsRef(plan.studentId, plan.id).add({ @@ -51,6 +52,7 @@ export class PlanItem implements SubscribableModel, PlanElement { planId: plan.id, type, completed: false, + image, lector: false, nameForChild: i18n.t('planItemActivity:taskNameForChild'), order: lastItemOrder + 1, @@ -63,6 +65,7 @@ export class PlanItem implements SubscribableModel, PlanElement { planId: plan.id, type, completed: false, + image, lector: false, nameForChild: i18n.t('planItemActivity:taskNameForChild'), order: lastItemOrder + 1, diff --git a/src/models/PlanSubItem.ts b/src/models/PlanSubItem.ts index 9179353..06405e6 100644 --- a/src/models/PlanSubItem.ts +++ b/src/models/PlanSubItem.ts @@ -30,6 +30,7 @@ export class PlanSubItem implements SubscribableModel, PlanElement { time!: number; type: PlanItemType = PlanItemType.SubElement; lector!: boolean; + image!: string; complete = () => { this.update({ completed: true }); diff --git a/src/screens/planItemActivity/ImagePicker.tsx b/src/screens/planItemActivity/ImagePicker.tsx index 59e8da2..6db67fb 100644 --- a/src/screens/planItemActivity/ImagePicker.tsx +++ b/src/screens/planItemActivity/ImagePicker.tsx @@ -1,25 +1,64 @@ -import React, { SFC } from 'react'; +import React, { PureComponent } from 'react'; import { StyleSheet, View } from 'react-native'; +import { Image } from 'react-native-elements'; +import { ImagePickerResponse } from 'react-native-image-picker'; import { Icon, ModalTrigger } from 'components'; import { i18n } from 'locale'; import { PlanItem } from 'models'; import { palette } from 'styles'; +import { loadImage, uploadImage } from '../../infrastructure/Images'; import { ImagePickerModal } from './ImagePickerModal'; interface Props { planItem: PlanItem; + setFieldValue: (field: string, value: any) => void; + submitForm: () => void; } -export const ImagePicker: SFC = ({ planItem }) => ( - - } title={i18n.t('planItemActivity:addImage')}> - - +interface State { + imageUri: string; +} + +export class ImagePicker extends PureComponent { + state = { + imageUri: '', + }; + + componentDidMount = async () => { + if (this.props.planItem && this.props.planItem.image) { + const imageUri = await loadImage(this.props.planItem.image); + this.setState({ imageUri }); + } + }; + + updateImage = async (image: ImagePickerResponse) => { + const imageName = await uploadImage(image.uri, image.fileName); + this.props.setFieldValue('image', imageName); + this.props.submitForm(); + this.setState({ imageUri: image.uri }); + }; + + render() { + const { planItem } = this.props; + return ( + + } + title={i18n.t('planItemActivity:addImage')} + > + + {planItem && this.state.imageUri ? ( + + ) : ( + + )} + + - - -); + ); + } +} const styles = StyleSheet.create({ container: { @@ -33,5 +72,11 @@ const styles = StyleSheet.create({ borderColor: palette.backgroundSurface, paddingHorizontal: 85, paddingVertical: 67, + flex: 1, + justifyContent: 'center', + }, + image: { + width: 200, + height: 200, }, }); diff --git a/src/screens/planItemActivity/ImagePickerModal.tsx b/src/screens/planItemActivity/ImagePickerModal.tsx index 46d2d39..69c2f2d 100644 --- a/src/screens/planItemActivity/ImagePickerModal.tsx +++ b/src/screens/planItemActivity/ImagePickerModal.tsx @@ -5,16 +5,18 @@ import { PlanItem } from 'models'; import { Route } from 'navigation'; import React, { SFC } from 'react'; import { StyleSheet, View } from 'react-native'; +import ImagePicker, { ImagePickerResponse } from 'react-native-image-picker'; import { NavigationService } from 'services'; import { dimensions } from 'styles'; import { ImageAction } from './ImageAction'; interface Props { closeModal?: () => void; + updateImage: (image: ImagePickerResponse) => void; planItem: PlanItem; } -export const ImagePickerModal: SFC = ({ closeModal = noop, planItem }) => { +export const ImagePickerModal: SFC = ({ closeModal = noop, planItem, updateImage }) => { const navigateToImageLibrary = () => { closeModal(); NavigationService.navigate(Route.ImageLibrary, { @@ -22,16 +24,44 @@ export const ImagePickerModal: SFC = ({ closeModal = noop, planItem }) => }); }; + const openCamera = () => { + closeModal(); + ImagePicker.launchCamera( + { + mediaType: 'photo', + }, + response => { + if (!response.didCancel && !response.error) { + updateImage(response); + } + }, + ); + }; + + const openGallery = () => { + closeModal(); + ImagePicker.launchImageLibrary( + { + mediaType: 'photo', + }, + response => { + if (!response.didCancel && !response.error) { + updateImage(response); + } + }, + ); + }; + return ( - + - + ); diff --git a/src/screens/planItemActivity/PlanItemForm.tsx b/src/screens/planItemActivity/PlanItemForm.tsx index d24c2cb..c1eb270 100644 --- a/src/screens/planItemActivity/PlanItemForm.tsx +++ b/src/screens/planItemActivity/PlanItemForm.tsx @@ -13,6 +13,7 @@ import { SimpleTask } from './SimpleTask'; export interface PlanItemFormData { name: string; nameForChild: string; + image: string; } interface Props { @@ -35,6 +36,7 @@ export class PlanItemForm extends React.PureComponent { ? this.props.planItem.name : `${i18n.t('planItemActivity:newTask')}${this.props.taskNumber}`, nameForChild: this.props.planItem ? this.props.planItem.nameForChild : '', + image: '', }; validationSchema = Yup.object().shape({ diff --git a/src/screens/planItemActivity/PlanItemTaskScreen.tsx b/src/screens/planItemActivity/PlanItemTaskScreen.tsx index 8f44baa..9d0f0ef 100644 --- a/src/screens/planItemActivity/PlanItemTaskScreen.tsx +++ b/src/screens/planItemActivity/PlanItemTaskScreen.tsx @@ -27,26 +27,28 @@ export class PlanItemTaskScreen extends React.PureComponent { + createPlanItem = async (formData: PlanItemFormData) => { + const { name, image } = formData; const plan = this.props.navigation.getParam('plan'); - const planItem = await PlanItem.createPlanItem(plan, PlanItemType.SimpleTask, name, this.getLastItemOrder()); + const planItem = await PlanItem.createPlanItem(plan, PlanItemType.SimpleTask, name, image, this.getLastItemOrder()); this.setState({ planItem }); }; updatePlanItem = async (formData: PlanItemFormData) => { - const { name, nameForChild } = formData; + const { name, nameForChild, image } = formData; await this.state.planItem.update({ name, nameForChild, + image, }); - this.setState({ planItem: { ...this.state.planItem, name, nameForChild } }); + this.setState({ planItem: { ...this.state.planItem, name, nameForChild, image } }); }; onSubmit = (formData: PlanItemFormData) => - this.state.planItem ? this.updatePlanItem(formData) : this.createPlanItem(formData.name); + this.state.planItem ? this.updatePlanItem(formData) : this.createPlanItem(formData); render() { const { planItem } = this.state; diff --git a/src/screens/planItemActivity/SimpleTask.tsx b/src/screens/planItemActivity/SimpleTask.tsx index f15bacf..de68a73 100644 --- a/src/screens/planItemActivity/SimpleTask.tsx +++ b/src/screens/planItemActivity/SimpleTask.tsx @@ -26,12 +26,12 @@ export class SimpleTask extends React.PureComponent { }; render() { - const { values, handleChange, submitForm } = this.props.formikProps; + const { values, handleChange, submitForm, setFieldValue } = this.props.formikProps; return ( - + { +interface State { + imageUri: string; +} + +export class PlanElementListItem extends React.PureComponent { + state = { + imageUri: '', + }; + + componentDidMount = async () => { + if (this.props.item.image) { + const imageUri = await loadImage(this.props.item.image); + this.setState({ imageUri }); + } + }; + container(): ViewStyle { return this.props.item.completed ? styles.containerCompleted : styles.container; } @@ -58,6 +75,7 @@ export class PlanElementListItem extends React.PureComponent { + {this.state.imageUri && } { +interface State { + imageUri: string; +} + +export class PlanSlideItem extends React.PureComponent { + state = { + imageUri: '', + }; + get showText(): boolean { const { type } = this.props; return type === StudentDisplayOption.ImageWithTextSlide || type === StudentDisplayOption.TextSlide; @@ -22,22 +32,14 @@ export class PlanSlideItem extends React.PureComponent { get showImage(): boolean { const { type } = this.props; - return type === StudentDisplayOption.ImageWithTextSlide || type === StudentDisplayOption.LargeImageSlide; + const imageMode = type === StudentDisplayOption.ImageWithTextSlide || type === StudentDisplayOption.LargeImageSlide; + return imageMode && !!this.props.planItem.image; } render() { - const { planItem } = this.props; return ( - {this.showImage && ( - - - - )} + {this.showImage && } {this.showText && ( { @@ -24,6 +27,8 @@ export class RunPlanSlideScreen extends React.PureComponent = new ModelSubscriber(); state: Readonly = { planItems: [], + planItemImageUri: '', + loadingImage: false, pageNumber: 0, student: this.props.navigation.getParam('student'), }; @@ -32,7 +37,12 @@ export class RunPlanSlideScreen extends React.PureComponent this.setState({ planItems })); + this.planItemsSubscriber.subscribeCollectionUpdates(plan, async planItems => { + if (planItems.length > this.state.pageNumber) { + await this.loadPlanItemImage(planItems[this.state.pageNumber]); + } + this.setState({ planItems }); + }); this.studentSubscriber.subscribeElementUpdates(student, updatedStudent => this.setState({ student: updatedStudent }), ); @@ -43,8 +53,19 @@ export class RunPlanSlideScreen extends React.PureComponent { + async loadPlanItemImage(planItem: PlanItem) { + let planItemImageUri = ''; + this.setState({ loadingImage: true }); + if (planItem.image) { + planItemImageUri = await loadImage(planItem.image); + } + this.setState({ planItemImageUri, loadingImage: false }); + } + + nextPage = async () => { if (this.state.pageNumber + 1 < this.state.planItems.length) { + const nextPlanItem = this.state.planItems[this.state.pageNumber + 1]; + await this.loadPlanItemImage(nextPlanItem); this.setState(state => ({ pageNumber: state.pageNumber + 1 })); } else { this.props.navigation.navigate(Route.Dashboard); @@ -63,6 +84,7 @@ export class RunPlanSlideScreen extends React.PureComponent @@ -76,7 +98,7 @@ export class RunPlanSlideScreen extends React.PureComponent= 2.1.2 < 3" +ieee754@^1.1.4: + version "1.1.13" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.13.tgz#ec168558e95aa181fd87d37f55c32bbcb6708b84" + integrity sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg== + ignore-walk@^3.0.1: version "3.0.3" resolved "https://registry.yarnpkg.com/ignore-walk/-/ignore-walk-3.0.3.tgz#017e2447184bfeade7c238e4aefdd1e8f95b1e37" @@ -4404,6 +4463,11 @@ kind-of@^6.0.0, kind-of@^6.0.2: resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.2.tgz#01146b36a6218e64e58f3a8d66de5d7fc6f6d051" integrity sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA== +kind-of@^6.0.1: + version "6.0.3" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" + integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== + klaw@^1.0.0: version "1.3.1" resolved "https://registry.yarnpkg.com/klaw/-/klaw-1.3.1.tgz#4088433b46b3b1ba259d78785d8e96f73ba02439" @@ -5136,6 +5200,14 @@ mixin-deep@^1.2.0: for-in "^1.0.2" is-extendable "^1.0.1" +mixin-object@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/mixin-object/-/mixin-object-2.0.1.tgz#4fb949441dab182540f1fe035ba60e1947a5e57e" + integrity sha1-T7lJRB2rGCVA8f4DW6YOGUel5X4= + dependencies: + for-in "^0.1.3" + is-extendable "^0.1.1" + mkdirp@0.x, mkdirp@^0.5.0, mkdirp@^0.5.1: version "0.5.1" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" @@ -5521,7 +5593,7 @@ open@^6.2.0: dependencies: is-wsl "^1.1.0" -opencollective-postinstall@^2.0.0, opencollective-postinstall@^2.0.2: +opencollective-postinstall@^2.0.0, opencollective-postinstall@^2.0.1, opencollective-postinstall@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/opencollective-postinstall/-/opencollective-postinstall-2.0.2.tgz#5657f1bede69b6e33a45939b061eb53d3c6c3a89" integrity sha512-pVOEP16TrAO2/fjej1IdOyupJY8KDUM1CvsaScRbw6oddvpQoOfGk4ywha0HKKVAD6RkW4x6Q+tNBwhf3Bgpuw== @@ -6009,6 +6081,13 @@ randexp@0.4.6: discontinuous-range "1.0.0" ret "~0.1.10" +randombytes@^2.0.3: + version "2.1.0" + resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" + integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== + dependencies: + safe-buffer "^5.1.0" + range-parser@~1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" @@ -6130,6 +6209,11 @@ react-native-image-crop-picker@^0.26.2: resolved "https://registry.yarnpkg.com/react-native-image-crop-picker/-/react-native-image-crop-picker-0.26.2.tgz#c70985ff6740e63569f90b185be0516d83f5933b" integrity sha512-KnPtSJklwXr/BNdcyAlDp9xkDCQyJ5ZA4dM10elBc2aIXac/4CndbiPFZrwu4GlAYYYKlkiTTwTYW+h4RyGLaw== +react-native-image-picker@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/react-native-image-picker/-/react-native-image-picker-2.3.1.tgz#1977b65e07793d2c0b2d1976fb675c5f75ad7f11" + integrity sha512-c/a2h7/T7yBo5KlNQhcSn4xf4+6Li6LfJ59+GZT1ZzzWrj/6X8DiJ/TJBOlOZMC5tJriZKuRkWSsr74k6z+brw== + react-native-localize@^1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/react-native-localize/-/react-native-localize-1.3.1.tgz#d0b7046acd4214ac2bcb61102317374351400c76" @@ -6187,6 +6271,13 @@ react-native-tab-view@^1.2.0, react-native-tab-view@^1.4.1: dependencies: prop-types "^15.6.1" +react-native-uuid@^1.4.9: + version "1.4.9" + resolved "https://registry.yarnpkg.com/react-native-uuid/-/react-native-uuid-1.4.9.tgz#a526742f8fddfe6414500655212ca8d109c40229" + integrity sha1-pSZ0L4/d/mQUUAZVISyo0QnEAik= + dependencies: + randombytes "^2.0.3" + react-native-vector-icons@^6.6.0: version "6.6.0" resolved "https://registry.yarnpkg.com/react-native-vector-icons/-/react-native-vector-icons-6.6.0.tgz#66cf004918eb05d90778d64bd42077c1800d481b" @@ -6622,7 +6713,7 @@ safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== -safe-buffer@^5.0.1, safe-buffer@^5.1.2, safe-buffer@~5.2.0: +safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.2, safe-buffer@~5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.0.tgz#b74daec49b1148f88c64b68d49b1e815c1f2f519" integrity sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg== @@ -6749,6 +6840,15 @@ setprototypeof@1.1.1: resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683" integrity sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw== +shallow-clone@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-1.0.0.tgz#4480cd06e882ef68b2ad88a3ea54832e2c48b571" + integrity sha512-oeXreoKR/SyNJtRJMAKPDSvd28OqEwG4eR/xc856cRGBII7gX9lvAqDxusPm0846z/w/hWYjI1NpKwJ00NHzRA== + dependencies: + is-extendable "^0.1.1" + kind-of "^5.0.0" + mixin-object "^2.0.1" + shebang-command@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" @@ -7143,6 +7243,14 @@ sudo-prompt@^9.0.0: resolved "https://registry.yarnpkg.com/sudo-prompt/-/sudo-prompt-9.0.0.tgz#eebedeee9fcd6f661324e6bb46335e3288e8dc8a" integrity sha512-kUn5fiOk0nhY2oKD9onIkcNCE4Zt85WTsvOfSmqCplmlEvXCcPOmp1npH5YWuf8Bmyy9wLWkIxx+D+8cThBORQ== +superstruct@^0.6.2: + version "0.6.2" + resolved "https://registry.yarnpkg.com/superstruct/-/superstruct-0.6.2.tgz#c5eb034806a17ff98d036674169ef85e4c7f6a1c" + integrity sha512-lvA97MFAJng3rfjcafT/zGTSWm6Tbpk++DP6It4Qg7oNaeM+2tdJMuVgGje21/bIpBEs6iQql1PJH6dKTjl4Ig== + dependencies: + clone-deep "^2.0.1" + kind-of "^6.0.1" + supports-color@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7"