diff --git a/__tests__/lib/string.test.ts b/__tests__/lib/string.test.ts
index 2aef90fea1..c5ae06591e 100644
--- a/__tests__/lib/string.test.ts
+++ b/__tests__/lib/string.test.ts
@@ -4,7 +4,7 @@ import {parseEmbedPlayerFromUrl} from 'lib/strings/embed-player'
import {
createStarterPackGooglePlayUri,
createStarterPackLinkFromAndroidReferrer,
- parseStarterPackHttpUri,
+ parseStarterPackUri,
} from 'lib/strings/starter-pack'
import {cleanError} from '../../src/lib/strings/errors'
import {createFullHandle, makeValidHandle} from '../../src/lib/strings/handles'
@@ -803,62 +803,129 @@ describe('parseEmbedPlayerFromUrl', () => {
})
describe('createStarterPackLinkFromAndroidReferrer', () => {
- const inputs = [
- 'utm_source=bluesky&utm_medium=starterpack&utm_content=starterpack-haileyok.com-rkey',
- 'utm_source=bluesky&utm_content=starterpack-haileyok.com-rkey&utm_medium=starterpack',
- 'utm_source=bluesky&utm_content=starterpack-haileyok.com-rkey',
- 'utm_content=starterpack-haileyok.com-rkey',
- 'utm_source=redsea&utm_content=starterpack-haileyok.com-rkey',
- 'utm_source=bluesky&utm_content=starterpack-haileyok.com',
- 'utm_source=bluesky&utm_content=starterpack',
- 'utm_source=bluesky&utm_content=nope',
- 'utm_source=bluesky',
- 'utm_content=starterpack-haileyok.com-rkey',
- ]
- const outputs = [
- 'https://bsky.app/start/haileyok.com/rkey',
- 'https://bsky.app/start/haileyok.com/rkey',
- 'https://bsky.app/start/haileyok.com/rkey',
- null,
- null,
- null,
- null,
- null,
- null,
- null,
- ]
+ const validOutput = 'https://bsky.app/start/haileyok.com/rkey'
- it('returns a starter pack link when input is valid', () => {
- for (let i = 0; i < inputs.length; i++) {
- const result = createStarterPackLinkFromAndroidReferrer(inputs[i])
- expect(result).toEqual(outputs[i])
- }
+ it('returns a link when input contains utm_source and utm_content', () => {
+ expect(
+ createStarterPackLinkFromAndroidReferrer(
+ 'utm_source=bluesky&utm_content=starterpack-haileyok.com-rkey',
+ ),
+ ).toEqual(validOutput)
+ })
+
+ it('returns a link when input contains utm_source and utm_content in different order', () => {
+ expect(
+ createStarterPackLinkFromAndroidReferrer(
+ 'utm_content=starterpack-haileyok.com-rkey&utm_source=bluesky',
+ ),
+ ).toEqual(validOutput)
+ })
+
+ it('returns a link when input contains other parameters as well', () => {
+ expect(
+ createStarterPackLinkFromAndroidReferrer(
+ 'utm_source=bluesky&utm_medium=starterpack&utm_content=starterpack-haileyok.com-rkey',
+ ),
+ ).toEqual(validOutput)
+ })
+
+ it('returns null when utm_source is not present', () => {
+ expect(
+ createStarterPackLinkFromAndroidReferrer(
+ 'utm_content=starterpack-haileyok.com-rkey',
+ ),
+ ).toEqual(null)
+ })
+
+ it('returns null when utm_content is not present', () => {
+ expect(
+ createStarterPackLinkFromAndroidReferrer('utm_source=bluesky'),
+ ).toEqual(null)
+ })
+
+ it('returns null when utm_content is malformed', () => {
+ expect(
+ createStarterPackLinkFromAndroidReferrer(
+ 'utm_content=starterpack-haileyok.com',
+ ),
+ ).toEqual(null)
+
+ expect(
+ createStarterPackLinkFromAndroidReferrer('utm_content=starterpack'),
+ ).toEqual(null)
+
+ expect(
+ createStarterPackLinkFromAndroidReferrer(
+ 'utm_content=starterpack-haileyok.com-rkey-more',
+ ),
+ ).toEqual(null)
+
+ expect(
+ createStarterPackLinkFromAndroidReferrer(
+ 'utm_content=notastarterpack-haileyok.com-rkey',
+ ),
+ ).toEqual(null)
})
})
describe('parseStarterPackHttpUri', () => {
- const inputs = [
- 'https://bsky.app/start/haileyok.com/rkey',
- 'https://bsky.app/start/haileyok.com/ilovetesting',
- 'https://bsky.app/start/testlover9000.com/rkey',
- 'https://bsky.app/start/testlover9000.com',
- 'https://bsky.app/start/testlover9000.com/rkey/other',
- 'https://bsky.app/start',
- ]
- const outputs = [
- {name: 'haileyok.com', rkey: 'rkey'},
- {name: 'haileyok.com', rkey: 'ilovetesting'},
- {name: 'testlover9000.com', rkey: 'rkey'},
- null,
- null,
- null,
- ]
+ const baseUri = 'https://bsky.app/start'
- it('returns the correct name and rkey when input is valid', () => {
- for (let i = 0; i < inputs.length; i++) {
- const result = parseStarterPackHttpUri(inputs[i])
- expect(result).toEqual(outputs[i])
- }
+ it('returns a valid at uri when http uri is valid', () => {
+ const validHttpUri = `${baseUri}/haileyok.com/rkey`
+ expect(parseStarterPackUri(validHttpUri)).toEqual({
+ name: 'haileyok.com',
+ rkey: 'rkey',
+ })
+
+ const validHttpUri2 = `${baseUri}/haileyok.com/ilovetesting`
+ expect(parseStarterPackUri(validHttpUri2)).toEqual({
+ name: 'haileyok.com',
+ rkey: 'ilovetesting',
+ })
+
+ const validHttpUri3 = `${baseUri}/testlover9000.com/rkey`
+ expect(parseStarterPackUri(validHttpUri3)).toEqual({
+ name: 'testlover9000.com',
+ rkey: 'rkey',
+ })
+ })
+
+ it('returns null when there is no rkey', () => {
+ const validHttpUri = `${baseUri}/haileyok.com`
+ expect(parseStarterPackUri(validHttpUri)).toEqual(null)
+ })
+
+ it('returns null when there is an extra path', () => {
+ const validHttpUri = `${baseUri}/haileyok.com/rkey/other`
+ expect(parseStarterPackUri(validHttpUri)).toEqual(null)
+ })
+
+ it('returns null when there is no handle or rkey', () => {
+ const validHttpUri = `${baseUri}`
+ expect(parseStarterPackUri(validHttpUri)).toEqual(null)
+ })
+
+ it('returns the at uri when the input is a valid starterpack at uri', () => {
+ const validAtUri = 'at://did:123/app.bsky.graph.starterpack/rkey'
+ expect(parseStarterPackUri(validAtUri)).toEqual({
+ name: 'did:123',
+ rkey: 'rkey',
+ })
+ })
+
+ it('returns null when the at uri has no rkey', () => {
+ const validAtUri = 'at://did:123/app.bsky.graph.starterpack'
+ expect(parseStarterPackUri(validAtUri)).toEqual(null)
+ })
+
+ it('returns null when the collection is not app.bsky.graph.starterpack', () => {
+ const validAtUri = 'at://did:123/app.bsky.graph.list/rkey'
+ expect(parseStarterPackUri(validAtUri)).toEqual(null)
+ })
+
+ it('returns null when the input is undefined', () => {
+ expect(parseStarterPackUri(undefined)).toEqual(null)
})
})
@@ -866,13 +933,24 @@ describe('createStarterPackGooglePlayUri', () => {
const base =
'https://play.google.com/store/apps/details?id=xyz.blueskyweb.app&referrer=utm_source%3Dbluesky%26utm_medium%3Dstarterpack%26utm_content%3Dstarterpack-'
- const inputs = [['name', 'rkey'], ['name'], []]
- const outputs = [base + 'name-rkey', null, null]
-
it('returns valid google play uri when input is valid', () => {
- for (let i = 0; i < inputs.length; i++) {
- const result = createStarterPackGooglePlayUri(inputs[i][0], inputs[i][1])
- expect(result).toEqual(outputs[i])
- }
+ expect(createStarterPackGooglePlayUri('name', 'rkey')).toEqual(
+ `${base}name-rkey`,
+ )
+ })
+
+ it('returns null when no rkey is supplied', () => {
+ // @ts-expect-error test
+ expect(createStarterPackGooglePlayUri('name', undefined)).toEqual(null)
+ })
+
+ it('returns null when no name or rkey are supplied', () => {
+ // @ts-expect-error test
+ expect(createStarterPackGooglePlayUri(undefined, undefined)).toEqual(null)
+ })
+
+ it('returns null when rkey is supplied but no name', () => {
+ // @ts-expect-error test
+ expect(createStarterPackGooglePlayUri(undefined, 'rkey')).toEqual(null)
})
})
diff --git a/src/components/hooks/useStarterPackEntry.native.ts b/src/components/hooks/useStarterPackEntry.native.ts
index 44299dafd2..a7304d64e4 100644
--- a/src/components/hooks/useStarterPackEntry.native.ts
+++ b/src/components/hooks/useStarterPackEntry.native.ts
@@ -2,21 +2,31 @@ import React from 'react'
import {createStarterPackLinkFromAndroidReferrer} from 'lib/strings/starter-pack'
import {isAndroid} from 'platform/detection'
-import {useSetCurrentStarterPack} from 'state/preferences/starter-pack'
-import {useUsedStarterPacks} from 'state/preferences/used-starter-packs'
+import {useHasCheckedForStarterPack} from 'state/preferences/used-starter-packs'
+import {useSetActiveStarterPack} from 'state/shell/starter-pack'
import SwissArmyKnife from '../../../modules/expo-bluesky-swiss-army'
import GooglePlayReferrer from '../../../modules/expo-google-play-referrer'
export function useStarterPackEntry() {
const [ready, setReady] = React.useState(false)
- const setCurrentStarterPack = useSetCurrentStarterPack()
- const usedStarterPacks = useUsedStarterPacks()
- const hasRan = React.useRef(false)
+ const setActiveStarterPack = useSetActiveStarterPack()
+ const hasCheckedForStarterPack = useHasCheckedForStarterPack()
React.useEffect(() => {
- if (ready || hasRan.current) return
+ if (ready) return
+
+ // On Android, we cannot clear the referral link. It gets stored for 90 days and all we can do is query for it. So,
+ // let's just ensure we never check again after the first time.
+ if (hasCheckedForStarterPack) {
+ setReady(true)
+ return
+ }
+
+ // Safety for Android. Very unlike this could happen, but just in case. The response should be nearly immediate
+ const timeout = setTimeout(() => {
+ setReady(true)
+ }, 500)
- hasRan.current = true
;(async () => {
let uri: string | null | undefined
@@ -28,17 +38,22 @@ export function useStarterPackEntry() {
}
} else {
uri = await SwissArmyKnife.getStringValueAsync('starterPackUri', true)
+ SwissArmyKnife.setStringValueAsync('starterPackUri', null, true)
}
- if (uri && !usedStarterPacks?.includes(uri)) {
- setCurrentStarterPack({
+ if (uri) {
+ setActiveStarterPack({
uri,
})
}
setReady(true)
})()
- }, [ready, setCurrentStarterPack, usedStarterPacks])
+
+ return () => {
+ clearTimeout(timeout)
+ }
+ }, [ready, setActiveStarterPack, hasCheckedForStarterPack])
return ready
}
diff --git a/src/components/hooks/useStarterPackEntry.ts b/src/components/hooks/useStarterPackEntry.ts
index dbe2c74862..3518fbced2 100644
--- a/src/components/hooks/useStarterPackEntry.ts
+++ b/src/components/hooks/useStarterPackEntry.ts
@@ -1,25 +1,25 @@
import React from 'react'
-import {parseStarterPackHttpUri} from 'lib/strings/starter-pack'
-import {useSetCurrentStarterPack} from 'state/preferences/starter-pack'
+import {httpStarterPackUriToAtUri} from 'lib/strings/starter-pack'
+import {useSetActiveStarterPack} from 'state/shell/starter-pack'
export function useStarterPackEntry() {
- const setCurrentStarterPack = useSetCurrentStarterPack()
+ const setActiveStarterPack = useSetActiveStarterPack()
React.useEffect(() => {
const href = window.location.href
- const parsed = parseStarterPackHttpUri(href)
+ const atUri = httpStarterPackUriToAtUri(href)
- if (parsed) {
+ if (atUri) {
const url = new URL(href)
+ // Determines if an App Clip is loading this landing page
const isClip = url.searchParams.get('clip') === 'true'
-
- setCurrentStarterPack({
- uri: href,
+ setActiveStarterPack({
+ uri: atUri,
isClip,
})
}
- }, [setCurrentStarterPack])
+ }, [setActiveStarterPack])
return true
}
diff --git a/src/lib/strings/starter-pack.ts b/src/lib/strings/starter-pack.ts
index 36f6c7802d..1836c435fd 100644
--- a/src/lib/strings/starter-pack.ts
+++ b/src/lib/strings/starter-pack.ts
@@ -1,3 +1,5 @@
+import {AtUri} from '@atproto/api'
+
import {makeStarterPackLink} from 'lib/routes/links'
export function createStarterPackLinkFromAndroidReferrer(
@@ -24,21 +26,35 @@ export function createStarterPackLinkFromAndroidReferrer(
}
}
-export function parseStarterPackHttpUri(uri: string): {
- name?: string
- rkey?: string
+export function parseStarterPackUri(uri?: string): {
+ name: string
+ rkey: string
} | null {
- try {
- const url = new URL(uri)
- const parts = url.pathname.split('/')
- const name = parts[2]
- const rkey = parts[3]
+ if (!uri) return null
- if (parts.length !== 4) return null
- if (!name || !rkey) return null
- return {
- name,
- rkey,
+ try {
+ if (uri.startsWith('at://')) {
+ const atUri = new AtUri(uri)
+ if (atUri.collection !== 'app.bsky.graph.starterpack') return null
+ if (atUri.rkey) {
+ return {
+ name: atUri.hostname,
+ rkey: atUri.rkey,
+ }
+ }
+ return null
+ } else {
+ const url = new URL(uri)
+ const parts = url.pathname.split('/')
+ const name = parts[2]
+ const rkey = parts[3]
+
+ if (parts.length !== 4) return null
+ if (!name || !rkey) return null
+ return {
+ name,
+ rkey,
+ }
}
} catch (e) {
return null
@@ -52,3 +68,14 @@ export function createStarterPackGooglePlayUri(
if (!name || !rkey) return null
return `https://play.google.com/store/apps/details?id=xyz.blueskyweb.app&referrer=utm_source%3Dbluesky%26utm_medium%3Dstarterpack%26utm_content%3Dstarterpack-${name}-${rkey}`
}
+
+export function httpStarterPackUriToAtUri(httpUri?: string): string | null {
+ if (!httpUri) return null
+
+ const parsed = parseStarterPackUri(httpUri)
+ if (!parsed) return null
+
+ if (httpUri.startsWith('at://')) return httpUri
+
+ return `at://${parsed.name}/app.bsky.graph.starterpack/${parsed.rkey}`
+}
diff --git a/src/screens/Login/LoginForm.tsx b/src/screens/Login/LoginForm.tsx
index dfa10668b6..7cfd38e34f 100644
--- a/src/screens/Login/LoginForm.tsx
+++ b/src/screens/Login/LoginForm.tsx
@@ -21,6 +21,7 @@ import {logger} from '#/logger'
import {useSessionApi} from '#/state/session'
import {useLoggedOutViewControls} from '#/state/shell/logged-out'
import {useRequestNotificationsPermission} from 'lib/notifications/notifications'
+import {useSetHasCheckedForStarterPack} from 'state/preferences/used-starter-packs'
import {atoms as a, useTheme} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import {FormError} from '#/components/forms/FormError'
@@ -69,6 +70,7 @@ export const LoginForm = ({
const {login} = useSessionApi()
const requestNotificationsPermission = useRequestNotificationsPermission()
const {setShowLoggedOut} = useLoggedOutViewControls()
+ const setHasCheckedForStarterPack = useSetHasCheckedForStarterPack()
const onPressSelectService = React.useCallback(() => {
Keyboard.dismiss()
@@ -116,6 +118,7 @@ export const LoginForm = ({
'LoginForm',
)
setShowLoggedOut(false)
+ setHasCheckedForStarterPack(true)
requestNotificationsPermission('Login')
} catch (e: any) {
const errMsg = e.toString()
diff --git a/src/screens/Onboarding/StepFinished.tsx b/src/screens/Onboarding/StepFinished.tsx
index 8df50d6ba9..b2fbd72efe 100644
--- a/src/screens/Onboarding/StepFinished.tsx
+++ b/src/screens/Onboarding/StepFinished.tsx
@@ -15,12 +15,12 @@ import {useAgent} from '#/state/session'
import {useOnboardingDispatch} from '#/state/shell'
import {uploadBlob} from 'lib/api'
import {useRequestNotificationsPermission} from 'lib/notifications/notifications'
-import {makeStarterPackLink} from 'lib/routes/links'
+import {useSetHasCheckedForStarterPack} from 'state/preferences/used-starter-packs'
+import {useSetSelectedFeed} from 'state/shell/selected-feed'
import {
- useCurrentStarterPack,
- useSetCurrentStarterPack,
-} from 'state/preferences/starter-pack'
-import {useAddUsedStarterPack} from 'state/preferences/used-starter-packs'
+ useActiveStarterPack,
+ useSetActiveStarterPack,
+} from 'state/shell/starter-pack'
import {
DescriptionText,
OnboardingControls,
@@ -48,18 +48,20 @@ export function StepFinished() {
const queryClient = useQueryClient()
const agent = useAgent()
const requestNotificationsPermission = useRequestNotificationsPermission()
- const currentStarterPack = useCurrentStarterPack()
- const setCurrentStarterPack = useSetCurrentStarterPack()
- const addUsedStarterPack = useAddUsedStarterPack()
+ const activeStarterPack = useActiveStarterPack()
+ const setActiveStarterPack = useSetActiveStarterPack()
+ const setHasCheckedForStarterPack = useSetHasCheckedForStarterPack()
+ const setSelectedFeed = useSetSelectedFeed()
const finishOnboarding = React.useCallback(async () => {
setSaving(true)
try {
let starterPack: AppBskyGraphDefs.StarterPackView | undefined
let listItems: AppBskyGraphDefs.ListItemView[] | undefined
- if (currentStarterPack) {
+
+ if (activeStarterPack?.uri) {
const spRes = await agent.app.bsky.graph.getStarterPack({
- starterPack: currentStarterPack.uri,
+ starterPack: activeStarterPack.uri,
})
starterPack = spRes.data.starterPack
@@ -92,17 +94,11 @@ export function StepFinished() {
pinned: true,
})),
)
- setCurrentStarterPack({
- uri: '',
- initialFeed: starterPack.feeds?.[0].uri,
- })
+ setSelectedFeed(`feedgen|${starterPack.feeds[0].uri}`)
} else {
- setCurrentStarterPack({
- uri: '',
- initialFeed: 'following',
- })
+ setSelectedFeed('following')
}
- addUsedStarterPack(makeStarterPackLink(starterPack))
+ setActiveStarterPack(undefined)
}
})(),
(async () => {
@@ -150,7 +146,7 @@ export function StepFinished() {
logger.error(e)
// If there was an error encountered, we need to just clear the starter pack so we don't break things for subsequent
// app restarts
- setCurrentStarterPack(undefined)
+ setActiveStarterPack(undefined)
// don't alert the user, just let them into their account
}
@@ -168,6 +164,7 @@ export function StepFinished() {
})
setSaving(false)
+ setHasCheckedForStarterPack(true)
dispatch({type: 'finish'})
onboardDispatch({type: 'finish'})
track('OnboardingV2:StepFinished:End')
@@ -179,11 +176,12 @@ export function StepFinished() {
dispatch,
onboardDispatch,
track,
- currentStarterPack,
+ activeStarterPack,
state,
requestNotificationsPermission,
- addUsedStarterPack,
- setCurrentStarterPack,
+ setActiveStarterPack,
+ setHasCheckedForStarterPack,
+ setSelectedFeed,
])
React.useEffect(() => {
diff --git a/src/screens/StarterPack/StarterPackLandingScreen.tsx b/src/screens/StarterPack/StarterPackLandingScreen.tsx
index d9d26a2b53..4f3da8bf93 100644
--- a/src/screens/StarterPack/StarterPackLandingScreen.tsx
+++ b/src/screens/StarterPack/StarterPackLandingScreen.tsx
@@ -12,18 +12,14 @@ import {useLingui} from '@lingui/react'
import {isAndroidWeb} from 'lib/browser'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
-import {
- createStarterPackGooglePlayUri,
- parseStarterPackHttpUri,
-} from 'lib/strings/starter-pack'
+import {createStarterPackGooglePlayUri} from 'lib/strings/starter-pack'
import {isWeb} from 'platform/detection'
import {useModerationOpts} from 'state/preferences/moderation-opts'
-import {
- useCurrentStarterPack,
- useSetCurrentStarterPack,
-} from 'state/preferences/starter-pack'
-import {useResolveDidQuery} from 'state/queries/resolve-uri'
import {useStarterPackQuery} from 'state/queries/useStarterPackQuery'
+import {
+ useActiveStarterPack,
+ useSetActiveStarterPack,
+} from 'state/shell/starter-pack'
import {LoggedOutScreenState} from 'view/com/auth/LoggedOut'
import {CenteredView} from 'view/com/util/Views'
import {Logo} from 'view/icons/Logo'
@@ -54,43 +50,15 @@ export function LandingScreen({
setScreenState,
}: {
setScreenState: (state: LoggedOutScreenState) => void
-}) {
- const currentStarterPack = useCurrentStarterPack()
- const parsed = parseStarterPackHttpUri(currentStarterPack?.uri || '')
-
- React.useEffect(() => {
- if (!parsed) {
- setScreenState(LoggedOutScreenState.S_LoginOrCreateAccount)
- }
- }, [parsed, setScreenState])
-
- if (!parsed) {
- return null
- }
-
- return
-}
-
-export function LandingScreenInner({
- setScreenState,
-}: {
- setScreenState: (state: LoggedOutScreenState) => void
}) {
const moderationOpts = useModerationOpts()
- const currentStarterPack = useCurrentStarterPack()
- const {name, rkey} =
- parseStarterPackHttpUri(currentStarterPack?.uri || '') ?? {}
+ const activeStarterPack = useActiveStarterPack()
- const {
- data: did,
- isLoading: isLoadingDid,
- isError: isErrorDid,
- } = useResolveDidQuery(name)
const {
data: starterPack,
isLoading: isLoadingStarterPack,
isError: isErrorStarterPack,
- } = useStarterPackQuery({did, rkey})
+ } = useStarterPackQuery({uri: activeStarterPack?.uri})
const isValid =
starterPack &&
@@ -98,17 +66,15 @@ export function LandingScreenInner({
AppBskyGraphStarterpack.validateRecord(starterPack.record)
React.useEffect(() => {
- if (isErrorDid || isErrorStarterPack || (starterPack && !isValid)) {
+ if (isErrorStarterPack || (starterPack && !isValid)) {
setScreenState(LoggedOutScreenState.S_LoginOrCreateAccount)
}
- }, [isErrorDid, isErrorStarterPack, setScreenState, isValid, starterPack])
+ }, [isErrorStarterPack, setScreenState, isValid, starterPack])
- if (!did || !starterPack || !isValid || !moderationOpts) {
+ if (!starterPack || !isValid || !moderationOpts) {
return (
)
}
@@ -136,8 +102,8 @@ function LandingScreenLoaded({
const {record, creator, listItemsSample, feeds, joinedWeekCount} = starterPack
const {_} = useLingui()
const t = useTheme()
- const currentStarterPack = useCurrentStarterPack()
- const setCurrentStarterPack = useSetCurrentStarterPack()
+ const activeStarterPack = useActiveStarterPack()
+ const setActiveStarterPack = useSetActiveStarterPack()
const {isTabletOrDesktop} = useWebMediaQueries()
const androidDialogControl = useDialogControl()
@@ -147,14 +113,14 @@ function LandingScreenLoaded({
const listItemsCount = starterPack.list?.listItemCount ?? 0
const onContinue = () => {
- setCurrentStarterPack({
+ setActiveStarterPack({
uri: starterPack.uri,
})
setScreenState(LoggedOutScreenState.S_CreateAccount)
}
const onJoinPress = () => {
- if (currentStarterPack?.isClip) {
+ if (activeStarterPack?.isClip) {
setAppClipOverlayVisible(true)
postAppClipMessage({
action: 'present',
@@ -185,7 +151,7 @@ function LandingScreenLoaded({
borderBottomLeftRadius: 10,
borderBottomRightRadius: 10,
},
- currentStarterPack?.isClip && {
+ activeStarterPack?.isClip && {
paddingTop: 100,
},
]}>
@@ -312,7 +278,7 @@ function LandingScreenLoaded({
size="medium"
style={[a.mt_2xl]}
onPress={() => {
- setCurrentStarterPack(undefined)
+ setActiveStarterPack(undefined)
setScreenState(LoggedOutScreenState.S_CreateAccount)
}}>
diff --git a/src/screens/StarterPack/Wizard/State.tsx b/src/screens/StarterPack/Wizard/State.tsx
index 9a4867548e..6b298a78cc 100644
--- a/src/screens/StarterPack/Wizard/State.tsx
+++ b/src/screens/StarterPack/Wizard/State.tsx
@@ -113,12 +113,10 @@ function reducer(state: State, action: Action): State {
export function Provider({
starterPack,
listItems,
- profile,
children,
}: {
starterPack?: AppBskyGraphDefs.StarterPackView
listItems?: AppBskyGraphDefs.ListItemView[]
- profile: AppBskyActorDefs.ProfileView
children: React.ReactNode
}) {
const createInitialState = (): State => {
@@ -138,7 +136,7 @@ export function Provider({
return {
canNext: true,
currentStep: 'Details',
- profiles: [profile],
+ profiles: [],
feeds: [],
processing: false,
transitionDirection: 'Forward',
diff --git a/src/screens/StarterPack/Wizard/index.tsx b/src/screens/StarterPack/Wizard/index.tsx
index 84280f2b24..b4a65cb698 100644
--- a/src/screens/StarterPack/Wizard/index.tsx
+++ b/src/screens/StarterPack/Wizard/index.tsx
@@ -127,7 +127,7 @@ export function Wizard({
}
return (
-
+
@@ -150,6 +151,7 @@ function WizardInner({
createdAt: initialCreatedAt,
listUri: initialListUri,
listItems: initialListItems,
+ profile,
moderationOpts,
}: {
did?: string
@@ -157,6 +159,7 @@ function WizardInner({
createdAt?: string
listUri?: string
listItems?: AppBskyGraphDefs.ListItemView[]
+ profile: AppBskyActorDefs.ProfileViewBasic
moderationOpts: ModerationOpts
}) {
const navigation = useNavigation()
@@ -470,6 +473,7 @@ function WizardInner({
onNext={onNext}
nextBtnText={currUiStrings.nextBtn}
moderationOpts={moderationOpts}
+ profile={profile}
/>
)}
@@ -552,10 +556,12 @@ function Footer({
onNext,
nextBtnText,
moderationOpts,
+ profile,
}: {
onNext: () => void
nextBtnText: string
moderationOpts: ModerationOpts
+ profile: AppBskyActorDefs.ProfileViewBasic
}) {
const {_} = useLingui()
const t = useTheme()
@@ -563,7 +569,10 @@ function Footer({
const editDialogControl = useDialogControl()
const {bottom: bottomInset} = useSafeAreaInsets()
- const items = state.currentStep === 'Profiles' ? state.profiles : state.feeds
+ const items =
+ state.currentStep === 'Profiles'
+ ? [profile, ...state.profiles]
+ : state.feeds
const initialNamesIndex = state.currentStep === 'Profiles' ? 1 : 0
const isEditEnabled =
diff --git a/src/state/persisted/schema.ts b/src/state/persisted/schema.ts
index be050bae3b..0813ad3f15 100644
--- a/src/state/persisted/schema.ts
+++ b/src/state/persisted/schema.ts
@@ -87,14 +87,7 @@ export const schema = z.object({
disableHaptics: z.boolean().optional(),
disableAutoplay: z.boolean().optional(),
kawaii: z.boolean().optional(),
- currentStarterPack: z
- .object({
- uri: z.string(),
- initialFeed: z.string().optional(),
- isClip: z.boolean().optional(),
- })
- .optional(),
- usedStarterPacks: z.array(z.string()).optional(),
+ hasCheckedForStarterPack: z.boolean().optional(),
/** @deprecated */
mutedThreads: z.array(z.string()),
})
@@ -135,6 +128,5 @@ export const defaults: Schema = {
disableHaptics: false,
disableAutoplay: prefersReducedMotion,
kawaii: false,
- currentStarterPack: undefined,
- usedStarterPacks: [],
+ hasCheckedForStarterPack: false,
}
diff --git a/src/state/preferences/index.tsx b/src/state/preferences/index.tsx
index 405c9969db..72ed0067bc 100644
--- a/src/state/preferences/index.tsx
+++ b/src/state/preferences/index.tsx
@@ -1,5 +1,6 @@
import React from 'react'
+import {Provider as StarterPackProvider} from '../shell/starter-pack'
import {Provider as AltTextRequiredProvider} from './alt-text-required'
import {Provider as AutoplayProvider} from './autoplay'
import {Provider as DisableHapticsProvider} from './disable-haptics'
@@ -8,7 +9,6 @@ import {Provider as HiddenPostsProvider} from './hidden-posts'
import {Provider as InAppBrowserProvider} from './in-app-browser'
import {Provider as KawaiiProvider} from './kawaii'
import {Provider as LanguagesProvider} from './languages'
-import {Provider as StarterPackProvider} from './starter-pack'
import {Provider as UsedStarterPacksProvider} from './used-starter-packs'
export {
diff --git a/src/state/preferences/starter-pack.tsx b/src/state/preferences/starter-pack.tsx
deleted file mode 100644
index 583c2b65f8..0000000000
--- a/src/state/preferences/starter-pack.tsx
+++ /dev/null
@@ -1,43 +0,0 @@
-import React from 'react'
-
-import * as persisted from '#/state/persisted'
-
-type StateContext =
- | {
- uri: string
- initialFeed?: string
- isClip?: boolean
- }
- | undefined
-type SetContext = (v: StateContext) => void
-
-const stateContext = React.createContext(undefined)
-const setContext = React.createContext((_: StateContext) => {})
-
-export function Provider({children}: {children: React.ReactNode}) {
- const [state, setState] = React.useState(() =>
- persisted.get('currentStarterPack'),
- )
-
- const setStateWrapped = (v: StateContext) => {
- setState(v)
- persisted.write('currentStarterPack', v)
- }
-
- React.useEffect(() => {
- return persisted.onUpdate(() => {
- setState(persisted.get('currentStarterPack'))
- })
- }, [])
-
- return (
-
-
- {children}
-
-
- )
-}
-
-export const useCurrentStarterPack = () => React.useContext(stateContext)
-export const useSetCurrentStarterPack = () => React.useContext(setContext)
diff --git a/src/state/preferences/used-starter-packs.tsx b/src/state/preferences/used-starter-packs.tsx
index b04698caca..8d5d9e8283 100644
--- a/src/state/preferences/used-starter-packs.tsx
+++ b/src/state/preferences/used-starter-packs.tsx
@@ -2,25 +2,25 @@ import React from 'react'
import * as persisted from '#/state/persisted'
-type StateContext = string[] | undefined
-type SetContext = (v: string) => void
+type StateContext = boolean | undefined
+type SetContext = (v: boolean) => void
-const stateContext = React.createContext([])
-const setContext = React.createContext((_: string) => {})
+const stateContext = React.createContext(false)
+const setContext = React.createContext((_: boolean) => {})
export function Provider({children}: {children: React.ReactNode}) {
const [state, setState] = React.useState(() =>
- persisted.get('usedStarterPacks'),
+ persisted.get('hasCheckedForStarterPack'),
)
- const setStateWrapped = (v: string) => {
- persisted.write('usedStarterPacks', [...(state ? state : []), v])
- setState(prev => [...(prev ? prev : []), v])
+ const setStateWrapped = (v: boolean) => {
+ setState(v)
+ persisted.write('hasCheckedForStarterPack', v)
}
React.useEffect(() => {
return persisted.onUpdate(() => {
- setState(persisted.get('usedStarterPacks'))
+ setState(persisted.get('hasCheckedForStarterPack'))
})
}, [])
@@ -33,5 +33,5 @@ export function Provider({children}: {children: React.ReactNode}) {
)
}
-export const useUsedStarterPacks = () => React.useContext(stateContext)
-export const useAddUsedStarterPack = () => React.useContext(setContext)
+export const useHasCheckedForStarterPack = () => React.useContext(stateContext)
+export const useSetHasCheckedForStarterPack = () => React.useContext(setContext)
diff --git a/src/state/queries/useStarterPackQuery.ts b/src/state/queries/useStarterPackQuery.ts
index ca659f43b4..0348c2e0ea 100644
--- a/src/state/queries/useStarterPackQuery.ts
+++ b/src/state/queries/useStarterPackQuery.ts
@@ -1,30 +1,38 @@
import {StarterPackView} from '@atproto/api/dist/client/types/app/bsky/graph/defs'
import {QueryClient, useQuery} from '@tanstack/react-query'
+import {httpStarterPackUriToAtUri} from 'lib/strings/starter-pack'
import {useAgent} from 'state/session'
const RQKEY_ROOT = 'starter-pack'
const RQKEY = (did?: string, rkey?: string) => [RQKEY_ROOT, did, rkey]
export function useStarterPackQuery({
+ uri,
did,
rkey,
}: {
+ uri?: string
did?: string
rkey?: string
}) {
const agent = useAgent()
- const uri = `at://${did}/app.bsky.graph.starterpack/${rkey}`
return useQuery({
queryKey: RQKEY(did, rkey),
queryFn: async () => {
+ if (!uri) {
+ uri = `at://${did}/app.bsky.graph.starterpack/${rkey}`
+ } else if (uri && !uri.startsWith('at://')) {
+ // TODO remove this assertion
+ uri = httpStarterPackUriToAtUri(uri) as string
+ }
const res = await agent.app.bsky.graph.getStarterPack({
starterPack: uri,
})
return res.data.starterPack
},
- enabled: Boolean(did) && Boolean(rkey),
+ enabled: Boolean(uri) || Boolean(did && rkey),
})
}
diff --git a/src/state/shell/starter-pack.tsx b/src/state/shell/starter-pack.tsx
new file mode 100644
index 0000000000..f564712f0e
--- /dev/null
+++ b/src/state/shell/starter-pack.tsx
@@ -0,0 +1,25 @@
+import React from 'react'
+
+type StateContext =
+ | {
+ uri: string
+ isClip?: boolean
+ }
+ | undefined
+type SetContext = (v: StateContext) => void
+
+const stateContext = React.createContext(undefined)
+const setContext = React.createContext((_: StateContext) => {})
+
+export function Provider({children}: {children: React.ReactNode}) {
+ const [state, setState] = React.useState()
+
+ return (
+
+ {children}
+
+ )
+}
+
+export const useActiveStarterPack = () => React.useContext(stateContext)
+export const useSetActiveStarterPack = () => React.useContext(setContext)
diff --git a/src/view/screens/Home.tsx b/src/view/screens/Home.tsx
index 7bfef1511d..11f6ded67e 100644
--- a/src/view/screens/Home.tsx
+++ b/src/view/screens/Home.tsx
@@ -22,12 +22,13 @@ import {useSelectedFeed, useSetSelectedFeed} from '#/state/shell/selected-feed'
import {useOTAUpdates} from 'lib/hooks/useOTAUpdates'
import {useRequestNotificationsPermission} from 'lib/notifications/notifications'
import {HomeTabNavigatorParams, NativeStackScreenProps} from 'lib/routes/types'
-import {
- useCurrentStarterPack,
- useSetCurrentStarterPack,
-} from 'state/preferences/starter-pack'
-import {useUsedStarterPacks} from 'state/preferences/used-starter-packs'
+import {parseStarterPackUri} from 'lib/strings/starter-pack'
+import {isWeb} from 'platform/detection'
import {useLoggedOutViewControls} from 'state/shell/logged-out'
+import {
+ useActiveStarterPack,
+ useSetActiveStarterPack,
+} from 'state/shell/starter-pack'
import {FeedPage} from 'view/com/feeds/FeedPage'
import {Pager, PagerRef, RenderTabBarFnProps} from 'view/com/pager/Pager'
import {CustomFeedEmptyState} from 'view/com/posts/CustomFeedEmptyState'
@@ -38,33 +39,37 @@ import {HomeHeader} from '../com/home/HomeHeader'
type Props = NativeStackScreenProps
export function HomeScreen(props: Props) {
+ const {navigation} = props
+ const {hasSession} = useSession()
const {data: preferences} = usePreferencesQuery()
const {data: pinnedFeedInfos, isLoading: isPinnedFeedsLoading} =
usePinnedFeedsInfos()
- const currentStarterPack = useCurrentStarterPack()
- const usedStarterPacks = useUsedStarterPacks()
+ const activeStarterPack = useActiveStarterPack()
+ const setActiveStarterPack = useSetActiveStarterPack()
const {setShowLoggedOut, requestSwitchToAccount} = useLoggedOutViewControls()
React.useEffect(() => {
- if (currentStarterPack && !currentStarterPack?.initialFeed) {
- // In test environments, the URL won't start with `https://bsky.app`, so we want to find it by the route
- try {
- const route = new URL(currentStarterPack.uri).pathname
- const foundIndex = usedStarterPacks?.findIndex(p => p.includes(route))
- if (foundIndex === -1) {
- setShowLoggedOut(true)
- requestSwitchToAccount({requestedAccount: 'starterpack'})
- }
- } catch {
- // Don't need to handle anything here, just put the user on the home screen
+ // This will be true if the app was launched with a starter pack referral.
+ if (activeStarterPack?.uri) {
+ if (hasSession) {
+ const parsed = parseStarterPackUri(activeStarterPack.uri)
+ if (!parsed) return
+ setActiveStarterPack(undefined)
+ navigation.navigate('StarterPack', parsed)
+ } else {
+ setShowLoggedOut(true)
+ requestSwitchToAccount({
+ requestedAccount: isWeb ? 'starterpack' : 'new',
+ })
}
}
}, [
+ hasSession,
setShowLoggedOut,
requestSwitchToAccount,
- currentStarterPack?.initialFeed,
- currentStarterPack,
- usedStarterPacks,
+ activeStarterPack,
+ setActiveStarterPack,
+ navigation,
])
if (preferences && pinnedFeedInfos && !isPinnedFeedsLoading) {
@@ -96,23 +101,9 @@ function HomeScreenReady({
[pinnedFeedInfos],
)
- const currentStarterPack = useCurrentStarterPack()
- const setCurrentStarterPack = useSetCurrentStarterPack()
-
- const starterPackInitialFeed = currentStarterPack?.initialFeed
- ? allFeeds.find(f => {
- if (currentStarterPack.initialFeed === 'following') {
- return f === 'following'
- } else {
- return f === `feedgen|${currentStarterPack.initialFeed}`
- }
- })
- : undefined
const rawSelectedFeed = useSelectedFeed() ?? allFeeds[0]
const setSelectedFeed = useSetSelectedFeed()
- const maybeFoundIndex = allFeeds.indexOf(
- starterPackInitialFeed ?? rawSelectedFeed,
- )
+ const maybeFoundIndex = allFeeds.indexOf(rawSelectedFeed)
const selectedIndex = Math.max(0, maybeFoundIndex)
const selectedFeed = allFeeds[selectedIndex]
const requestNotificationsPermission = useRequestNotificationsPermission()
@@ -138,12 +129,7 @@ function HomeScreenReady({
lastPagerReportedIndexRef.current = selectedIndex
pagerRef.current?.setPage(selectedIndex, 'desktop-sidebar-click')
}
- }, [
- selectedIndex,
- allFeeds,
- setCurrentStarterPack,
- currentStarterPack?.initialFeed,
- ])
+ }, [selectedIndex, allFeeds])
const {hasSession} = useSession()
const setMinimalShellMode = useSetMinimalShellMode()