import {useCallback, useState} from 'react' import {View} from 'react-native' import { type AppBskyActorDefs, type AppBskyActorProfile, type AppBskyGraphDefs, AppBskyGraphStarterpack, type Un$Typed, } from '@atproto/api' import {TID} from '@atproto/common-web' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {Trans} from '@lingui/react/macro' 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 {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, useAppviewClient} from '#/state/session' import {useOnboardingDispatch} from '#/state/shell' import { useActiveStarterPack, useSetActiveStarterPack, } from '#/state/shell/landing' import {useProgressGuideControls} from '#/state/shell/progress-guide' import { OnboardingControls, OnboardingHeaderSlot, } from '#/screens/Onboarding/Layout' import { type OnboardingState, useOnboardingInternalState, } from '#/screens/Onboarding/state' import {bulkWriteFollows} from '#/screens/Onboarding/util' import {atoms as a, useBreakpoints} from '#/alf' import {Button, ButtonIcon, ButtonText} from '#/components/Button' import {ArrowRight_Stroke2_Corner0_Rounded as ArrowRight} from '#/components/icons/Arrow' import {Loader} from '#/components/Loader' import {useAnalytics} from '#/analytics' import {IS_WEB} from '#/env' import * as bsky from '#/types/bsky' import {ValuePropositionPager} from './ValuePropositionPager' export function StepFinished() { const {state, dispatch} = useOnboardingInternalState() const ax = useAnalytics() const onboardDispatch = useOnboardingDispatch() const [saving, setSaving] = useState(false) const queryClient = useQueryClient() const agent = useAgent() const appviewClient = useAppviewClient() const requestNotificationsPermission = useRequestNotificationsPermission() const activeStarterPack = useActiveStarterPack() const setActiveStarterPack = useSetActiveStarterPack() const setHasCheckedForStarterPack = useSetHasCheckedForStarterPack() const {startProgressGuide} = useProgressGuideControls() const finishOnboarding = 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( appviewClient, 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) ?? [])], starterPack ? {uri: starterPack.uri, cid: starterPack.cid} : undefined, ), (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: AppBskyActorDefs.SavedFeed[] = [ { ...DISCOVER_SAVED_FEED, id: TID.nextStr(), }, { ...TIMELINE_SAVED_FEED, id: TID.nextStr(), }, { ...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 = '' if (!next.createdAt) { next.createdAt = new Date().toISOString() } return next }) ax.metric('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('follow-10') dispatch({type: 'finish'}) onboardDispatch({type: 'finish'}) ax.metric('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) { ax.metric('starterPack:followAll', { logContext: 'Onboarding', starterPack: starterPack.uri, count: listItems?.length, }) } }, [ ax, queryClient, agent, appviewClient, dispatch, onboardDispatch, activeStarterPack, state, requestNotificationsPermission, setActiveStarterPack, setHasCheckedForStarterPack, startProgressGuide, ]) return ( ) } function ValueProposition({ finishOnboarding, saving, state, }: { finishOnboarding: () => void saving: boolean state: OnboardingState }) { const [subStep, setSubStep] = useState<0 | 1 | 2>(0) const {_} = useLingui() const ax = useAnalytics() const {gtMobile} = useBreakpoints() const onPress = () => { if (subStep === 2) { finishOnboarding() // has its own metrics } else if (subStep === 1) { setSubStep(2) ax.metric('onboarding:valueProp:stepTwo:nextPressed', {}) } else if (subStep === 0) { setSubStep(1) ax.metric('onboarding:valueProp:stepOne:nextPressed', {}) } } return ( <> {!gtMobile && ( )} setSubStep(ss)} avatarUri={state.profileStepResults.imageUri} /> {gtMobile && (IS_WEB ? subStep !== 2 : true) && ( )} ) }