diff --git a/package.json b/package.json index 516428f802..97fe301456 100644 --- a/package.json +++ b/package.json @@ -185,6 +185,7 @@ "react-native-uitextview": "^1.1.6", "react-native-url-polyfill": "^1.3.0", "react-native-uuid": "^2.0.1", + "react-native-view-shot": "^3.8.0", "react-native-web": "~0.19.6", "react-native-web-webview": "^1.0.2", "react-native-webview": "13.6.4", diff --git a/src/components/Dialog/index.tsx b/src/components/Dialog/index.tsx index e5a6792db6..b5258c02b9 100644 --- a/src/components/Dialog/index.tsx +++ b/src/components/Dialog/index.tsx @@ -213,7 +213,8 @@ export function Inner({children, style}: DialogInnerProps) { return ( { setSaving(true) + // TODO uncomment const { interestsStepResults, suggestedAccountsStepResults, algoFeedsStepResults, topicalFeedsStepResults, + profileStepResults, } = state const {selectedInterests} = interestsStepResults const selectedFeeds = [ @@ -110,6 +113,26 @@ export function StepFinished() { } })(), ]) + + if (gate('reduced_onboarding_and_home_algo')) { + await getAgent().upsertProfile(async existing => { + existing = existing ?? {} + + if (profileStepResults.imageUri && profileStepResults.imageMime) { + const res = await uploadBlob( + getAgent(), + profileStepResults.imageUri, + profileStepResults.imageMime, + ) + + if (res.data.blob) { + existing.avatar = res.data.blob + } + } + + return existing + }) + } } catch (e: any) { logger.info(`onboarding: bulk save failed`) logger.error(e) diff --git a/src/screens/Onboarding/StepInterests/index.tsx b/src/screens/Onboarding/StepInterests/index.tsx index 174488a34f..d6678f4b0c 100644 --- a/src/screens/Onboarding/StepInterests/index.tsx +++ b/src/screens/Onboarding/StepInterests/index.tsx @@ -31,8 +31,8 @@ import {Text} from '#/components/Typography' export function StepInterests() { const {_} = useLingui() const t = useTheme() - const {track} = useAnalytics() const {gtMobile} = useBreakpoints() + const {track} = useAnalytics() const {state, dispatch, interestsDisplayNames} = React.useContext(Context) const [saving, setSaving] = React.useState(false) const [interests, setInterests] = React.useState( diff --git a/src/screens/Onboarding/StepProfile/AvatarCircle.tsx b/src/screens/Onboarding/StepProfile/AvatarCircle.tsx new file mode 100644 index 0000000000..1be38b0d5f --- /dev/null +++ b/src/screens/Onboarding/StepProfile/AvatarCircle.tsx @@ -0,0 +1,77 @@ +import React from 'react' +import {View} from 'react-native' +import {Image as ExpoImage} from 'expo-image' +import {msg} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {AvatarCreatorCircle} from '#/screens/Onboarding/StepProfile/AvatarCreatorCircle' +import {useAvatar} from '#/screens/Onboarding/StepProfile/index' +import {atoms as a, useTheme} from '#/alf' +import {Button, ButtonIcon} from '#/components/Button' +import {Pencil_Stroke2_Corner0_Rounded as Pencil} from '#/components/icons/Pencil' +import {StreamingLive_Stroke2_Corner0_Rounded as StreamingLive} from '#/components/icons/StreamingLive' + +export function AvatarCircle({ + openLibrary, + openCreator, +}: { + openLibrary: () => unknown + openCreator: () => unknown +}) { + const {_} = useLingui() + const t = useTheme() + const {avatar} = useAvatar() + + const styles = React.useMemo( + () => ({ + imageContainer: [ + a.rounded_full, + a.overflow_hidden, + a.align_center, + a.justify_center, + a.border, + t.atoms.border_contrast_low, + t.atoms.bg_contrast_25, + { + height: 200, + width: 200, + }, + ], + }), + [t.atoms.bg_contrast_25, t.atoms.border_contrast_low], + ) + + return ( + + {avatar.useCreatedAvatar ? ( + + ) : avatar.image ? ( + + ) : ( + + + + )} + + + + + ) +} diff --git a/src/screens/Onboarding/StepProfile/AvatarCreatorCircle.tsx b/src/screens/Onboarding/StepProfile/AvatarCreatorCircle.tsx new file mode 100644 index 0000000000..1cd68eb61b --- /dev/null +++ b/src/screens/Onboarding/StepProfile/AvatarCreatorCircle.tsx @@ -0,0 +1,43 @@ +import React from 'react' +import {View} from 'react-native' + +import {Avatar} from '#/screens/Onboarding/StepProfile/index' +import {atoms as a, useTheme} from '#/alf' + +export function AvatarCreatorCircle({ + avatar, + size = 125, +}: { + avatar: Avatar + size?: number +}) { + const t = useTheme() + const Icon = avatar.placeholder.component + + const styles = React.useMemo( + () => ({ + imageContainer: [ + a.rounded_full, + a.overflow_hidden, + a.align_center, + a.justify_center, + a.border, + t.atoms.border_contrast_high, + { + height: size, + width: size, + backgroundColor: avatar.backgroundColor, + }, + ], + }), + [avatar.backgroundColor, size, t.atoms.border_contrast_high], + ) + + return ( + + + + + + ) +} diff --git a/src/screens/Onboarding/StepProfile/AvatarCreatorItems.tsx b/src/screens/Onboarding/StepProfile/AvatarCreatorItems.tsx new file mode 100644 index 0000000000..98c01ce7dc --- /dev/null +++ b/src/screens/Onboarding/StepProfile/AvatarCreatorItems.tsx @@ -0,0 +1,145 @@ +import React from 'react' +import {View} from 'react-native' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {Avatar} from '#/screens/Onboarding/StepProfile/index' +import { + AvatarColor, + avatarColors, + emojiItems, + EmojiName, + emojiNames, +} from '#/screens/Onboarding/StepProfile/types' +import {atoms as a, useTheme} from '#/alf' +import {Button, ButtonIcon} from '#/components/Button' +import {Text} from '#/components/Typography' + +const ACTIVE_BORDER_WIDTH = 3 +const ACTIVE_BORDER_STYLES = { + top: -ACTIVE_BORDER_WIDTH, + bottom: -ACTIVE_BORDER_WIDTH, + left: -ACTIVE_BORDER_WIDTH, + right: -ACTIVE_BORDER_WIDTH, + opacity: 0.5, + borderWidth: 3, +} + +export function AvatarCreatorItems({ + type, + avatar, + setAvatar, +}: { + type: 'emojis' | 'colors' + avatar: Avatar + setAvatar: React.Dispatch> +}) { + const {_} = useLingui() + const t = useTheme() + const isEmojis = type === 'emojis' + + const onSelectEmoji = React.useCallback( + (emoji: EmojiName) => { + setAvatar(prev => ({ + ...prev, + placeholder: emojiItems[emoji], + })) + }, + [setAvatar], + ) + + const onSelectColor = React.useCallback( + (color: AvatarColor) => { + setAvatar(prev => ({ + ...prev, + backgroundColor: color, + })) + }, + [setAvatar], + ) + + return ( + + + {isEmojis ? ( + Select an emoji + ) : ( + Select a color + )} + + + + {isEmojis + ? emojiNames.map(emojiName => ( + + )) + : avatarColors.map(color => ( + + ))} + + + ) +} diff --git a/src/screens/Onboarding/StepProfile/PlaceholderCanvas.tsx b/src/screens/Onboarding/StepProfile/PlaceholderCanvas.tsx new file mode 100644 index 0000000000..29ba39a0b4 --- /dev/null +++ b/src/screens/Onboarding/StepProfile/PlaceholderCanvas.tsx @@ -0,0 +1,67 @@ +import React from 'react' +import {View} from 'react-native' +import ViewShot from 'react-native-view-shot' + +import {useAvatar} from '#/screens/Onboarding/StepProfile/index' +import {atoms as a} from '#/alf' + +const SIZE_MULTIPLIER = 1.5 + +export interface PlaceholderCanvasRef { + capture: () => Promise +} + +// This component is supposed to be invisible to the user. We only need this for ViewShot to have something to +// "screenshot". +export const PlaceholderCanvas = React.forwardRef( + function PlaceholderCanvas({}, ref) { + const {avatar} = useAvatar() + const viewshotRef = React.useRef() + const Icon = avatar.placeholder.component + + const styles = React.useMemo( + () => ({ + container: [a.absolute, {top: -2000}], + imageContainer: [ + a.align_center, + a.justify_center, + {height: 150 * SIZE_MULTIPLIER, width: 150 * SIZE_MULTIPLIER}, + ], + }), + [], + ) + + React.useImperativeHandle(ref, () => ({ + // @ts-ignore this library doesn't have types + capture: viewshotRef.current.capture, + })) + + return ( + + + + + + + + ) + }, +) diff --git a/src/screens/Onboarding/StepProfile/index.tsx b/src/screens/Onboarding/StepProfile/index.tsx index 8db3e77616..bf47bbc95b 100644 --- a/src/screens/Onboarding/StepProfile/index.tsx +++ b/src/screens/Onboarding/StepProfile/index.tsx @@ -1,55 +1,319 @@ import React from 'react' import {View} from 'react-native' +import {Image as ExpoImage} from 'expo-image' +import { + ImagePickerOptions, + launchImageLibraryAsync, + MediaTypeOptions, +} from 'expo-image-picker' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' +import {useAnalytics} from '#/lib/analytics/analytics' +import {logEvent} from '#/lib/statsig/statsig' +import {usePhotoLibraryPermission} from 'lib/hooks/usePermissions' +import {compressIfNeeded} from 'lib/media/manip' +import {openCropper} from 'lib/media/picker' +import {getDataUriSize} from 'lib/media/util' +import {isNative, isWeb} from 'platform/detection' import { DescriptionText, OnboardingControls, TitleText, } from '#/screens/Onboarding/Layout' import {Context} from '#/screens/Onboarding/state' -import {atoms as a} from '#/alf' +import {AvatarCircle} from '#/screens/Onboarding/StepProfile/AvatarCircle' +import {AvatarCreatorCircle} from '#/screens/Onboarding/StepProfile/AvatarCreatorCircle' +import {AvatarCreatorItems} from '#/screens/Onboarding/StepProfile/AvatarCreatorItems' +import { + PlaceholderCanvas, + PlaceholderCanvasRef, +} from '#/screens/Onboarding/StepProfile/PlaceholderCanvas' +import {atoms as a, useBreakpoints, useTheme} from '#/alf' import {Button, ButtonIcon, ButtonText} from '#/components/Button' +import * as Dialog from '#/components/Dialog' import {IconCircle} from '#/components/IconCircle' import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components/icons/Chevron' +import {CircleInfo_Stroke2_Corner0_Rounded} from '#/components/icons/CircleInfo' import {StreamingLive_Stroke2_Corner0_Rounded as StreamingLive} from '#/components/icons/StreamingLive' +import {Text} from '#/components/Typography' +import {AvatarColor, avatarColors, Emoji, emojiItems} from './types' + +export interface Avatar { + image?: { + path: string + mime: string + size: number + width: number + height: number + } + backgroundColor: AvatarColor + placeholder: Emoji + useCreatedAvatar: boolean +} + +interface IAvatarContext { + avatar: Avatar + setAvatar: React.Dispatch> +} + +const AvatarContext = React.createContext({} as IAvatarContext) +export const useAvatar = () => React.useContext(AvatarContext) + +const randomColor = + avatarColors[Math.floor(Math.random() * avatarColors.length)] export function StepProfile() { const {_} = useLingui() - const {dispatch} = React.useContext(Context) + const t = useTheme() + const {gtMobile} = useBreakpoints() + const {track} = useAnalytics() + const {requestPhotoAccessIfNeeded} = usePhotoLibraryPermission() + const creatorControl = Dialog.useDialogControl() + const [error, setError] = React.useState('') + + const {state, dispatch} = React.useContext(Context) + const [avatar, setAvatar] = React.useState({ + image: state.profileStepResults?.image, + placeholder: emojiItems.at, + backgroundColor: randomColor, + useCreatedAvatar: false, + }) + + const canvasRef = React.useRef(null) + + React.useEffect(() => { + track('OnboardingV2:StepProfile:Start') + }, [track]) + + const openPicker = React.useCallback( + async (opts?: ImagePickerOptions) => { + const response = await launchImageLibraryAsync({ + exif: false, + mediaTypes: MediaTypeOptions.Images, + quality: 1, + ...opts, + }) + + return (response.assets ?? []) + .slice(0, 1) + .filter(asset => { + if ( + !asset.mimeType?.startsWith('image/') || + (!asset.mimeType?.endsWith('jpeg') && + !asset.mimeType?.endsWith('jpg') && + !asset.mimeType?.endsWith('png')) + ) { + setError(_(msg`Only .jpg and .png files are supported`)) + return false + } + return true + }) + .map(image => ({ + mime: 'image/jpeg', + height: image.height, + width: image.width, + path: image.uri, + size: getDataUriSize(image.uri), + })) + }, + [_, setError], + ) + + const onContinue = React.useCallback(async () => { + let imageUri = avatar?.image?.path + if (!imageUri || avatar.useCreatedAvatar) { + imageUri = await canvasRef.current?.capture() + } + + if (imageUri) { + dispatch({ + type: 'setProfileStepResults', + image: avatar.image, + imageUri, + imageMime: avatar.image?.mime ?? 'image/jpeg', + }) + } - const onContinue = React.useCallback(() => { dispatch({type: 'next'}) - }, [dispatch]) + track('OnboardingV2:StepProfile:End') + logEvent('onboarding:profile:nextPressed', {}) + }, [avatar.image, avatar.useCreatedAvatar, dispatch, track]) + + const onDoneCreating = React.useCallback(() => { + setAvatar(prev => ({ + ...prev, + useCreatedAvatar: true, + })) + creatorControl.close() + }, [creatorControl]) + + const openLibrary = React.useCallback(async () => { + if (!(await requestPhotoAccessIfNeeded())) { + return + } + + setError('') + + const items = await openPicker({ + aspect: [1, 1], + }) + let image = items[0] + if (!image) return + + if (!isWeb) { + image = await openCropper({ + mediaType: 'photo', + cropperCircleOverlay: true, + height: image.height, + width: image.width, + path: image.path, + }) + } + image = await compressIfNeeded(image, 1000000) + + // If we are on mobile, prefetching the image will load the image into memory before we try and display it, + // stopping any brief flickers. + if (isNative) { + await ExpoImage.prefetch(image.path) + } + + setAvatar(prev => ({ + ...prev, + image, + useCreatedAvatar: false, + })) + }, [requestPhotoAccessIfNeeded, setAvatar, openPicker, setError]) + + const onSecondaryPress = React.useCallback(() => { + if (avatar.useCreatedAvatar) { + openLibrary() + } else { + creatorControl.open() + } + }, [avatar.useCreatedAvatar, creatorControl, openLibrary]) + + const value = React.useMemo( + () => ({ + avatar, + setAvatar, + }), + [avatar], + ) return ( - - + + + + + Give your profile a face + + + + Help people know you're not a bot by uploading a picture or creating + an avatar. + + + + - - Give your profile a face - - - - Help people know you're not a bot by uploading a picture or creating - an avatar. - - + {error && ( + + + {error} + + )} + - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + ) } diff --git a/src/screens/Onboarding/StepProfile/types.ts b/src/screens/Onboarding/StepProfile/types.ts new file mode 100644 index 0000000000..92a82f101d --- /dev/null +++ b/src/screens/Onboarding/StepProfile/types.ts @@ -0,0 +1,148 @@ +import {Alien_Stroke2_Corner0_Rounded as Alien} from '#/components/icons/Alien' +import {Apple_Stroke2_Corner0_Rounded as Apple} from '#/components/icons/Apple' +import {At_Stroke2_Corner0_Rounded as At} from '#/components/icons/At' +import {Atom_Stroke2_Corner0_Rounded as Atom} from '#/components/icons/Atom' +import {Celebrate_Stroke2_Corner0_Rounded as Celebrate} from '#/components/icons/Celebrate' +import {Coffee_Stroke2_Corner0_Rounded as Coffee} from '#/components/icons/Coffee' +import { + EmojiArc_Stroke2_Corner0_Rounded as EmojiArc, + EmojiHeartEyes_Stroke2_Corner0_Rounded as EmojiHeartEyes, +} from '#/components/icons/Emoji' +import {Explosion_Stroke2_Corner0_Rounded as Explosion} from '#/components/icons/Explosion' +import {GameController_Stroke2_Corner0_Rounded as GameController} from '#/components/icons/GameController' +import {Lab_Stroke2_Corner0_Rounded as Lab} from '#/components/icons/Lab' +import {Leaf_Stroke2_Corner0_Rounded as Leaf} from '#/components/icons/Leaf' +import {MusicNote_Stroke2_Corner0_Rounded as MusicNote} from '#/components/icons/MusicNote' +import {PiggyBank_Stroke2_Corner0_Rounded as PiggyBank} from '#/components/icons/PiggyBank' +import {Pizza_Stroke2_Corner0_Rounded as Pizza} from '#/components/icons/Pizza' +import {Poop_Stroke2_Corner0_Rounded as Poop} from '#/components/icons/Poop' +import {Rose_Stroke2_Corner0_Rounded as Rose} from '#/components/icons/Rose' +import {Shaka_Stroke2_Corner0_Rounded as Shaka} from '#/components/icons/Shaka' +import {UFO_Stroke2_Corner0_Rounded as UFO} from '#/components/icons/UFO' +import {Zap_Stroke2_Corner0_Rounded as Zap} from '#/components/icons/Zap' + +/** + * If you want to add or remove icons from the selection, just add the name to the `emojiNames` array and + * add the item to the `emojiItems` record.. + */ + +export const emojiNames = [ + 'at', + 'arc', + 'heartEyes', + 'alien', + 'apple', + 'atom', + 'celebrate', + 'coffee', + 'gameController', + 'leaf', + 'musicNote', + 'pizza', + 'rose', + 'shaka', + 'ufo', + 'zap', + 'explosion', + 'lab', + 'piggyBank', + 'poop', +] as const +export type EmojiName = (typeof emojiNames)[number] + +export interface Emoji { + name: EmojiName + component: typeof EmojiArc +} +export const emojiItems: Record = { + at: { + name: 'at', + component: At, + }, + arc: { + name: 'arc', + component: EmojiArc, + }, + heartEyes: { + name: 'heartEyes', + component: EmojiHeartEyes, + }, + alien: { + name: 'alien', + component: Alien, + }, + apple: { + name: 'apple', + component: Apple, + }, + atom: { + name: 'atom', + component: Atom, + }, + celebrate: { + name: 'celebrate', + component: Celebrate, + }, + coffee: { + name: 'coffee', + component: Coffee, + }, + gameController: { + name: 'gameController', + component: GameController, + }, + leaf: { + name: 'leaf', + component: Leaf, + }, + musicNote: { + name: 'musicNote', + component: MusicNote, + }, + pizza: { + name: 'pizza', + component: Pizza, + }, + rose: { + name: 'rose', + component: Rose, + }, + shaka: { + name: 'shaka', + component: Shaka, + }, + ufo: { + name: 'ufo', + component: UFO, + }, + zap: { + name: 'zap', + component: Zap, + }, + explosion: { + name: 'explosion', + component: Explosion, + }, + lab: { + name: 'lab', + component: Lab, + }, + piggyBank: { + name: 'piggyBank', + component: PiggyBank, + }, + poop: { + name: 'poop', + component: Poop, + }, +} + +export const avatarColors = [ + '#FE8311', + '#FED811', + '#73DF84', + '#1185FE', + '#EF75EA', + '#F55454', +] as const +export type AvatarColor = (typeof avatarColors)[number] diff --git a/src/screens/Onboarding/StepSuggestedAccounts/index.tsx b/src/screens/Onboarding/StepSuggestedAccounts/index.tsx index 7b2ad2b999..774f2d3b01 100644 --- a/src/screens/Onboarding/StepSuggestedAccounts/index.tsx +++ b/src/screens/Onboarding/StepSuggestedAccounts/index.tsx @@ -69,9 +69,9 @@ export function Inner({ export function StepSuggestedAccounts() { const {_} = useLingui() + const {gtMobile} = useBreakpoints() const {track} = useAnalytics() const {state, dispatch, interestsDisplayNames} = React.useContext(Context) - const {gtMobile} = useBreakpoints() const suggestedDids = React.useMemo(() => { return aggregateInterestItems( state.interestsStepResults.selectedInterests, diff --git a/src/screens/Onboarding/state.ts b/src/screens/Onboarding/state.ts index 9452fbbc71..3031dfbf48 100644 --- a/src/screens/Onboarding/state.ts +++ b/src/screens/Onboarding/state.ts @@ -13,6 +13,7 @@ export type OnboardingState = { | 'algoFeeds' | 'topicalFeeds' | 'moderation' + | 'profile' | 'finished' activeStepIndex: number @@ -30,6 +31,13 @@ export type OnboardingState = { feedUris: string[] } profileStepResults: { + image?: { + path: string + mime: string + size: number + width: number + height: number + } imageUri?: string imageMime?: string } @@ -64,6 +72,7 @@ export type OnboardingAction = } | { type: 'setProfileStepResults' + image?: OnboardingState['profileStepResults']['image'] imageUri: string imageMime: string } @@ -80,7 +89,7 @@ export type ApiResponseMap = { export const initialState: OnboardingState = { hasPrev: false, - totalSteps: 7, + totalSteps: 8, activeStep: 'interests', activeStepIndex: 1, @@ -102,6 +111,7 @@ export const initialState: OnboardingState = { feedUris: [], }, profileStepResults: { + image: undefined, imageUri: '', imageMime: '', }, @@ -168,8 +178,11 @@ export function reducer( next.activeStep = 'moderation' next.activeStepIndex = 6 } else if (s.activeStep === 'moderation') { - next.activeStep = 'finished' + next.activeStep = 'profile' next.activeStepIndex = 7 + } else if (s.activeStep === 'profile') { + next.activeStep = 'finished' + next.activeStepIndex = 8 } break } @@ -189,9 +202,12 @@ export function reducer( } else if (s.activeStep === 'moderation') { next.activeStep = 'topicalFeeds' next.activeStepIndex = 5 - } else if (s.activeStep === 'finished') { + } else if (s.activeStep === 'profile') { next.activeStep = 'moderation' next.activeStepIndex = 6 + } else if (s.activeStep === 'finished') { + next.activeStep = 'profile' + next.activeStepIndex = 7 } break } @@ -226,6 +242,14 @@ export function reducer( } break } + case 'setProfileStepResults': { + next.profileStepResults = { + image: a.image, + imageUri: a.imageUri, + imageMime: a.imageMime, + } + break + } } const state = { @@ -243,6 +267,7 @@ export function reducer( suggestedAccountsStepResults: state.suggestedAccountsStepResults, algoFeedsStepResults: state.algoFeedsStepResults, topicalFeedsStepResults: state.topicalFeedsStepResults, + profileStepResults: state.profileStepResults, }) if (s.activeStep !== state.activeStep) { @@ -276,6 +301,7 @@ export const initialStateReduced: OnboardingState = { feedUris: [], }, profileStepResults: { + image: undefined, imageUri: '', imageMime: '', }, @@ -330,6 +356,7 @@ export function reducerReduced( } case 'setProfileStepResults': { next.profileStepResults = { + image: a.image, imageUri: a.imageUri, imageMime: a.imageMime, } diff --git a/yarn.lock b/yarn.lock index 4f4968b7d2..6df2993f4a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9114,6 +9114,11 @@ base-64@0.1.0, base-64@^0.1.0: resolved "https://registry.yarnpkg.com/base-64/-/base-64-0.1.0.tgz#780a99c84e7d600260361511c4877613bf24f6bb" integrity sha512-Y5gU45svrR5tI2Vt/X9GPd3L0HNIKzGu202EjxrXMpuc2V2CiKgemAbUUsqYmZJvPtCXoUKjNZwBJzsNScUbXA== +base64-arraybuffer@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz#1c37589a7c4b0746e34bd1feb951da2df01c1bdc" + integrity sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ== + base64-js@^1.0.2, base64-js@^1.2.3, base64-js@^1.3.1, base64-js@^1.5.1: version "1.5.1" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" @@ -10239,6 +10244,13 @@ css-in-js-utils@^3.1.0: dependencies: hyphenate-style-name "^1.0.3" +css-line-break@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/css-line-break/-/css-line-break-2.1.0.tgz#bfef660dfa6f5397ea54116bb3cb4873edbc4fa0" + integrity sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w== + dependencies: + utrie "^1.0.2" + css-loader@^6.5.1: version "6.8.1" resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-6.8.1.tgz#0f8f52699f60f5e679eab4ec0fcd68b8e8a50a88" @@ -13233,6 +13245,14 @@ html-webpack-plugin@^5.5.0: pretty-error "^4.0.0" tapable "^2.0.0" +html2canvas@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/html2canvas/-/html2canvas-1.4.1.tgz#7cef1888311b5011d507794a066041b14669a543" + integrity sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA== + dependencies: + css-line-break "^2.1.0" + text-segmentation "^1.0.3" + htmlparser2@^6.1.0: version "6.1.0" resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-6.1.0.tgz#c4d762b6c3371a05dbe65e94ae43a9f845fb8fb7" @@ -18827,6 +18847,13 @@ react-native-uuid@^2.0.1: resolved "https://registry.yarnpkg.com/react-native-uuid/-/react-native-uuid-2.0.1.tgz#ed4e2dfb1683eddb66967eb5dca140dfe1abddb9" integrity sha512-cptnoIbL53GTCrWlb/+jrDC6tvb7ypIyzbXNJcpR3Vab0mkeaaVd5qnB3f0whXYzS+SMoSQLcUUB0gEWqkPC0g== +react-native-view-shot@^3.8.0: + version "3.8.0" + resolved "https://registry.yarnpkg.com/react-native-view-shot/-/react-native-view-shot-3.8.0.tgz#1aa1905f0e79428ca32bf80c16fd4abc719c600b" + integrity sha512-4cU8SOhMn3YQIrskh+5Q8VvVRxQOu8/s1M9NAL4z5BY1Rm0HXMWkQJ4N0XsZ42+Yca+y86ISF3LC5qdLPvPuiA== + dependencies: + html2canvas "^1.4.1" + react-native-web-webview@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/react-native-web-webview/-/react-native-web-webview-1.0.2.tgz#c215efa70c17589f2c8d640b1f1dc669b18c6e02" @@ -20831,6 +20858,13 @@ test-exclude@^6.0.0: glob "^7.1.4" minimatch "^3.0.4" +text-segmentation@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/text-segmentation/-/text-segmentation-1.0.3.tgz#52a388159efffe746b24a63ba311b6ac9f2d7943" + integrity sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw== + dependencies: + utrie "^1.0.2" + text-table@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" @@ -21486,6 +21520,13 @@ utils-merge@1.0.1: resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== +utrie@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/utrie/-/utrie-1.0.2.tgz#d42fe44de9bc0119c25de7f564a6ed1b2c87a645" + integrity sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw== + dependencies: + base64-arraybuffer "^1.0.2" + uuid@^3.0.1, uuid@^3.3.2: version "3.4.0" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"