import React from 'react' import {View} from 'react-native' import { type AppBskyActorProfile, type AppBskyGraphDefs, AppBskyGraphStarterpack, type Un$Typed, } from '@atproto/api' import {type SavedFeed} from '@atproto/api/dist/client/types/app/bsky/actor/defs' import {TID} from '@atproto/common-web' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' import {useQueryClient} from '@tanstack/react-query' import {uploadBlob} from '#/lib/api' import { BSKY_APP_ACCOUNT_DID, DISCOVER_SAVED_FEED, TIMELINE_SAVED_FEED, VIDEO_SAVED_FEED, } from '#/lib/constants' import {useRequestNotificationsPermission} from '#/lib/notifications/notifications' import {logEvent, useGate} from '#/lib/statsig/statsig' import {logger} from '#/logger' import {useSetHasCheckedForStarterPack} from '#/state/preferences/used-starter-packs' import {getAllListMembers} from '#/state/queries/list-members' import {preferencesQueryKey} from '#/state/queries/preferences' import {RQKEY as profileRQKey} from '#/state/queries/profile' import {useAgent} from '#/state/session' import {useOnboardingDispatch} from '#/state/shell' import {useProgressGuideControls} from '#/state/shell/progress-guide' import { useActiveStarterPack, useSetActiveStarterPack, } from '#/state/shell/starter-pack' import { DescriptionText, OnboardingControls, TitleText, } from '#/screens/Onboarding/Layout' import {Context} from '#/screens/Onboarding/state' import {bulkWriteFollows} from '#/screens/Onboarding/util' import {atoms as a, useTheme} from '#/alf' import {Button, ButtonIcon, ButtonText} from '#/components/Button' import {IconCircle} from '#/components/IconCircle' import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check' import {Growth_Stroke2_Corner0_Rounded as Growth} from '#/components/icons/Growth' import {News2_Stroke2_Corner0_Rounded as News} from '#/components/icons/News2' import {Trending2_Stroke2_Corner2_Rounded as Trending} from '#/components/icons/Trending' import {Loader} from '#/components/Loader' import {Text} from '#/components/Typography' import * as bsky from '#/types/bsky' export function StepFinished() { const {_} = useLingui() const t = useTheme() const {state, dispatch} = React.useContext(Context) const onboardDispatch = useOnboardingDispatch() const [saving, setSaving] = React.useState(false) const queryClient = useQueryClient() const agent = useAgent() const requestNotificationsPermission = useRequestNotificationsPermission() const activeStarterPack = useActiveStarterPack() const setActiveStarterPack = useSetActiveStarterPack() const setHasCheckedForStarterPack = useSetHasCheckedForStarterPack() const {startProgressGuide} = useProgressGuideControls() const gate = useGate() const finishOnboarding = React.useCallback(async () => { setSaving(true) let starterPack: AppBskyGraphDefs.StarterPackView | undefined let listItems: AppBskyGraphDefs.ListItemView[] | undefined if (activeStarterPack?.uri) { try { const spRes = await agent.app.bsky.graph.getStarterPack({ starterPack: activeStarterPack.uri, }) starterPack = spRes.data.starterPack } catch (e) { logger.error('Failed to fetch starter pack', {safeMessage: e}) // don't tell the user, just get them through onboarding. } try { if (starterPack?.list) { listItems = await getAllListMembers(agent, starterPack.list.uri) } } catch (e) { logger.error('Failed to fetch starter pack list items', { safeMessage: e, }) // don't tell the user, just get them through onboarding. } } try { const {interestsStepResults, profileStepResults} = state const {selectedInterests} = interestsStepResults await Promise.all([ bulkWriteFollows(agent, [ BSKY_APP_ACCOUNT_DID, ...(listItems?.map(i => i.subject.did) ?? []), ]), (async () => { // Interests need to get saved first, then we can write the feeds to prefs await agent.setInterestsPref({tags: selectedInterests}) // Default feeds that every user should have pinned when landing in the app const feedsToSave: SavedFeed[] = [ { ...DISCOVER_SAVED_FEED, id: TID.nextStr(), }, { ...TIMELINE_SAVED_FEED, id: TID.nextStr(), }, ] if (gate('onboarding_add_video_feed')) { feedsToSave.push({ ...VIDEO_SAVED_FEED, id: TID.nextStr(), }) } // Any starter pack feeds will be pinned _after_ the defaults if (starterPack && starterPack.feeds?.length) { feedsToSave.push( ...starterPack.feeds.map(f => ({ type: 'feed', value: f.uri, pinned: true, id: TID.nextStr(), })), ) } await agent.overwriteSavedFeeds(feedsToSave) })(), (async () => { const {imageUri, imageMime} = profileStepResults const blobPromise = imageUri && imageMime ? uploadBlob(agent, imageUri, imageMime) : undefined await agent.upsertProfile(async existing => { let next: Un$Typed = existing ?? {} if (blobPromise) { const res = await blobPromise if (res.data.blob) { next.avatar = res.data.blob } } if (starterPack) { next.joinedViaStarterPack = { uri: starterPack.uri, cid: starterPack.cid, } } next.displayName = '' // HACKFIX // creating a bunch of identical profile objects is breaking the relay // tossing this unspecced field onto it to reduce the size of the problem // -prf next.createdAt = new Date().toISOString() return next }) logEvent('onboarding:finished:avatarResult', { avatarResult: profileStepResults.isCreatedAvatar ? 'created' : profileStepResults.image ? 'uploaded' : 'default', }) })(), requestNotificationsPermission('AfterOnboarding'), ]) } catch (e: any) { logger.info(`onboarding: bulk save failed`) logger.error(e) // don't alert the user, just let them into their account } // Try to ensure that prefs and profile are up-to-date by the time we render Home. await Promise.all([ queryClient.invalidateQueries({ queryKey: preferencesQueryKey, }), queryClient.invalidateQueries({ queryKey: profileRQKey(agent.session?.did ?? ''), }), ]).catch(e => { logger.error(e) // Keep going. }) setSaving(false) setActiveStarterPack(undefined) setHasCheckedForStarterPack(true) startProgressGuide( gate('old_postonboarding') ? 'like-10-and-follow-7' : 'follow-10', ) dispatch({type: 'finish'}) onboardDispatch({type: 'finish'}) logEvent('onboarding:finished:nextPressed', { usedStarterPack: Boolean(starterPack), starterPackName: starterPack && bsky.dangerousIsType( starterPack.record, AppBskyGraphStarterpack.isRecord, ) ? starterPack.record.name : undefined, starterPackCreator: starterPack?.creator.did, starterPackUri: starterPack?.uri, profilesFollowed: listItems?.length ?? 0, feedsPinned: starterPack?.feeds?.length ?? 0, }) if (starterPack && listItems?.length) { logEvent('starterPack:followAll', { logContext: 'Onboarding', starterPack: starterPack.uri, count: listItems?.length, }) } }, [ queryClient, agent, dispatch, onboardDispatch, activeStarterPack, state, requestNotificationsPermission, setActiveStarterPack, setHasCheckedForStarterPack, startProgressGuide, gate, ]) return ( You're ready to go! We hope you have a wonderful time. Remember, Bluesky is: Public Your posts, likes, and blocks are public. Mutes are private. Open Never lose access to your followers or data. Flexible Choose the algorithms that power your custom feeds. ) }