about summary refs log tree commit diff
path: root/src/screens
diff options
context:
space:
mode:
Diffstat (limited to 'src/screens')
-rw-r--r--src/screens/Log.tsx128
-rw-r--r--src/screens/Moderation/index.tsx19
-rw-r--r--src/screens/PostThread/components/ThreadItemReadMore.tsx6
-rw-r--r--src/screens/Profile/Header/ProfileHeaderStandard.tsx329
-rw-r--r--src/screens/Profile/Header/Shell.tsx2
-rw-r--r--src/screens/Profile/Header/SuggestedFollows.tsx45
-rw-r--r--src/screens/Settings/AppIconSettings/index.tsx16
-rw-r--r--src/screens/Settings/AppIconSettings/types.ts5
-rw-r--r--src/screens/Settings/AppIconSettings/useAppIconSets.ts10
-rw-r--r--src/screens/Settings/AppearanceSettings.tsx4
-rw-r--r--src/screens/Signup/StepHandle/index.tsx6
11 files changed, 391 insertions, 179 deletions
diff --git a/src/screens/Log.tsx b/src/screens/Log.tsx
new file mode 100644
index 000000000..2dd7fe84c
--- /dev/null
+++ b/src/screens/Log.tsx
@@ -0,0 +1,128 @@
+import {useCallback, useState} from 'react'
+import {LayoutAnimation, View} from 'react-native'
+import {Pressable} from 'react-native'
+import {msg, Trans} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+import {useFocusEffect} from '@react-navigation/native'
+
+import {useGetTimeAgo} from '#/lib/hooks/useTimeAgo'
+import {
+  type CommonNavigatorParams,
+  type NativeStackScreenProps,
+} from '#/lib/routes/types'
+import {getEntries} from '#/logger/logDump'
+import {useTickEveryMinute} from '#/state/shell'
+import {useSetMinimalShellMode} from '#/state/shell'
+import {atoms as a, useTheme} from '#/alf'
+import {
+  ChevronBottom_Stroke2_Corner0_Rounded as ChevronBottomIcon,
+  ChevronTop_Stroke2_Corner0_Rounded as ChevronTopIcon,
+} from '#/components/icons/Chevron'
+import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfoIcon} from '#/components/icons/CircleInfo'
+import {Warning_Stroke2_Corner0_Rounded as WarningIcon} from '#/components/icons/Warning'
+import * as Layout from '#/components/Layout'
+import {Text} from '#/components/Typography'
+
+export function LogScreen({}: NativeStackScreenProps<
+  CommonNavigatorParams,
+  'Log'
+>) {
+  const t = useTheme()
+  const {_} = useLingui()
+  const setMinimalShellMode = useSetMinimalShellMode()
+  const [expanded, setExpanded] = useState<string[]>([])
+  const timeAgo = useGetTimeAgo()
+  const tick = useTickEveryMinute()
+
+  useFocusEffect(
+    useCallback(() => {
+      setMinimalShellMode(false)
+    }, [setMinimalShellMode]),
+  )
+
+  const toggler = (id: string) => () => {
+    LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)
+    if (expanded.includes(id)) {
+      setExpanded(expanded.filter(v => v !== id))
+    } else {
+      setExpanded([...expanded, id])
+    }
+  }
+
+  return (
+    <Layout.Screen>
+      <Layout.Header.Outer>
+        <Layout.Header.BackButton />
+        <Layout.Header.Content>
+          <Layout.Header.TitleText>
+            <Trans>System log</Trans>
+          </Layout.Header.TitleText>
+        </Layout.Header.Content>
+        <Layout.Header.Slot />
+      </Layout.Header.Outer>
+      <Layout.Content>
+        {getEntries()
+          .slice(0)
+          .map(entry => {
+            return (
+              <View key={`entry-${entry.id}`}>
+                <Pressable
+                  style={[
+                    a.flex_row,
+                    a.align_center,
+                    a.py_md,
+                    a.px_sm,
+                    a.border_b,
+                    t.atoms.border_contrast_low,
+                    t.atoms.bg,
+                    a.gap_sm,
+                  ]}
+                  onPress={toggler(entry.id)}
+                  accessibilityLabel={_(msg`View debug entry`)}
+                  accessibilityHint={_(
+                    msg`Opens additional details for a debug entry`,
+                  )}>
+                  {entry.level === 'warn' || entry.level === 'error' ? (
+                    <WarningIcon size="sm" fill={t.palette.negative_500} />
+                  ) : (
+                    <CircleInfoIcon size="sm" />
+                  )}
+                  <Text style={[a.flex_1]}>{String(entry.message)}</Text>
+                  {entry.metadata &&
+                    Object.keys(entry.metadata).length > 0 &&
+                    (expanded.includes(entry.id) ? (
+                      <ChevronTopIcon
+                        size="sm"
+                        style={[t.atoms.text_contrast_low]}
+                      />
+                    ) : (
+                      <ChevronBottomIcon
+                        size="sm"
+                        style={[t.atoms.text_contrast_low]}
+                      />
+                    ))}
+                  <Text style={[{minWidth: 40}, t.atoms.text_contrast_medium]}>
+                    {timeAgo(entry.timestamp, tick)}
+                  </Text>
+                </Pressable>
+                {expanded.includes(entry.id) && (
+                  <View
+                    style={[
+                      t.atoms.bg_contrast_25,
+                      a.rounded_xs,
+                      a.p_sm,
+                      a.border_b,
+                      t.atoms.border_contrast_low,
+                    ]}>
+                    <View style={[a.px_sm, a.py_xs]}>
+                      <Text>{JSON.stringify(entry.metadata, null, 2)}</Text>
+                    </View>
+                  </View>
+                )}
+              </View>
+            )
+          })}
+      </Layout.Content>
+    </Layout.Screen>
+  )
+}
diff --git a/src/screens/Moderation/index.tsx b/src/screens/Moderation/index.tsx
index 1517792a1..983919c64 100644
--- a/src/screens/Moderation/index.tsx
+++ b/src/screens/Moderation/index.tsx
@@ -22,6 +22,7 @@ import {
 import {isNonConfigurableModerationAuthority} from '#/state/session/additional-moderation-authorities'
 import {useSetMinimalShellMode} from '#/state/shell'
 import {atoms as a, useBreakpoints, useTheme, type ViewStyleProp} from '#/alf'
+import {Admonition} from '#/components/Admonition'
 import {AgeAssuranceAdmonition} from '#/components/ageAssurance/AgeAssuranceAdmonition'
 import {Button, ButtonText} from '#/components/Button'
 import * as Dialog from '#/components/Dialog'
@@ -201,6 +202,24 @@ export function ModerationScreenInner({
 
   return (
     <View style={[a.pt_2xl, a.px_lg, gtMobile && a.px_2xl]}>
+      {isDeclaredUnderage && (
+        <View style={[a.pb_2xl]}>
+          <Admonition type="tip" style={[a.pb_md]}>
+            <Trans>
+              Your declared age is under 18. Some settings below may be
+              disabled. If this was a mistake, you may edit your birthdate in
+              your{' '}
+              <InlineLinkText
+                to="/settings/account"
+                label={_(msg`Go to account settings`)}>
+                account settings
+              </InlineLinkText>
+              .
+            </Trans>
+          </Admonition>
+        </View>
+      )}
+
       <Text
         style={[a.text_md, a.font_bold, a.pb_md, t.atoms.text_contrast_high]}>
         <Trans>Moderation tools</Trans>
diff --git a/src/screens/PostThread/components/ThreadItemReadMore.tsx b/src/screens/PostThread/components/ThreadItemReadMore.tsx
index 22ae63395..66ec11cb7 100644
--- a/src/screens/PostThread/components/ThreadItemReadMore.tsx
+++ b/src/screens/PostThread/components/ThreadItemReadMore.tsx
@@ -90,10 +90,10 @@ export const ThreadItemReadMore = memo(function ThreadItemReadMore({
                   interacted && a.underline,
                 ]}>
                 <Trans>
-                  Read {item.moreReplies} more{' '}
+                  Read{' '}
                   <Plural
-                    one="reply"
-                    other="replies"
+                    one="# more reply"
+                    other="# more replies"
                     value={item.moreReplies}
                   />
                 </Trans>
diff --git a/src/screens/Profile/Header/ProfileHeaderStandard.tsx b/src/screens/Profile/Header/ProfileHeaderStandard.tsx
index 2f61ba4df..32111dd3b 100644
--- a/src/screens/Profile/Header/ProfileHeaderStandard.tsx
+++ b/src/screens/Profile/Header/ProfileHeaderStandard.tsx
@@ -1,4 +1,4 @@
-import React, {memo, useMemo} from 'react'
+import {memo, useCallback, useMemo, useState} from 'react'
 import {View} from 'react-native'
 import {
   type AppBskyActorDefs,
@@ -40,6 +40,7 @@ import {EditProfileDialog} from './EditProfileDialog'
 import {ProfileHeaderHandle} from './Handle'
 import {ProfileHeaderMetrics} from './Metrics'
 import {ProfileHeaderShell} from './Shell'
+import {AnimatedProfileHeaderSuggestedFollows} from './SuggestedFollows'
 
 interface Props {
   profile: AppBskyActorDefs.ProfileViewDetailed
@@ -73,6 +74,7 @@ let ProfileHeaderStandard = ({
   const [_queueBlock, queueUnblock] = useProfileBlockMutationQueue(profile)
   const unblockPromptControl = Prompt.usePromptControl()
   const requireAuth = useRequireAuth()
+  const [showSuggestedFollows, setShowSuggestedFollows] = useState(false)
   const isBlockedUser =
     profile.viewer?.blocking ||
     profile.viewer?.blockedBy ||
@@ -81,6 +83,7 @@ let ProfileHeaderStandard = ({
   const editProfileControl = useDialogControl()
 
   const onPressFollow = () => {
+    setShowSuggestedFollows(true)
     requireAuth(async () => {
       try {
         await queueFollow()
@@ -102,6 +105,7 @@ let ProfileHeaderStandard = ({
   }
 
   const onPressUnfollow = () => {
+    setShowSuggestedFollows(false)
     requireAuth(async () => {
       try {
         await queueUnfollow()
@@ -122,7 +126,7 @@ let ProfileHeaderStandard = ({
     })
   }
 
-  const unblockAccount = React.useCallback(async () => {
+  const unblockAccount = useCallback(async () => {
     try {
       await queueUnblock()
       Toast.show(_(msg({message: 'Account unblocked', context: 'toast'})))
@@ -155,174 +159,185 @@ let ProfileHeaderStandard = ({
   }, [profile])
 
   return (
-    <ProfileHeaderShell
-      profile={profile}
-      moderation={moderation}
-      hideBackButton={hideBackButton}
-      isPlaceholderProfile={isPlaceholderProfile}>
-      <View
-        style={[a.px_lg, a.pt_md, a.pb_sm, a.overflow_hidden]}
-        pointerEvents={isIOS ? 'auto' : 'box-none'}>
+    <>
+      <ProfileHeaderShell
+        profile={profile}
+        moderation={moderation}
+        hideBackButton={hideBackButton}
+        isPlaceholderProfile={isPlaceholderProfile}>
         <View
-          style={[
-            {paddingLeft: 90},
-            a.flex_row,
-            a.align_center,
-            a.justify_end,
-            a.gap_xs,
-            a.pb_sm,
-            a.flex_wrap,
-          ]}
+          style={[a.px_lg, a.pt_md, a.pb_sm, a.overflow_hidden]}
           pointerEvents={isIOS ? 'auto' : 'box-none'}>
-          {isMe ? (
-            <>
-              <Button
-                testID="profileHeaderEditProfileButton"
-                size="small"
-                color="secondary"
-                variant="solid"
-                onPress={editProfileControl.open}
-                label={_(msg`Edit profile`)}
-                style={[a.rounded_full]}>
-                <ButtonText>
-                  <Trans>Edit Profile</Trans>
-                </ButtonText>
-              </Button>
-              <EditProfileDialog
-                profile={profile}
-                control={editProfileControl}
-              />
-            </>
-          ) : profile.viewer?.blocking ? (
-            profile.viewer?.blockingByList ? null : (
-              <Button
-                testID="unblockBtn"
-                size="small"
-                color="secondary"
-                variant="solid"
-                label={_(msg`Unblock`)}
-                disabled={!hasSession}
-                onPress={() => unblockPromptControl.open()}
-                style={[a.rounded_full]}>
-                <ButtonText>
-                  <Trans context="action">Unblock</Trans>
-                </ButtonText>
-              </Button>
-            )
-          ) : !profile.viewer?.blockedBy ? (
-            <>
-              {hasSession && subscriptionsAllowed && (
-                <SubscribeProfileButton
+          <View
+            style={[
+              {paddingLeft: 90},
+              a.flex_row,
+              a.align_center,
+              a.justify_end,
+              a.gap_xs,
+              a.pb_sm,
+              a.flex_wrap,
+            ]}
+            pointerEvents={isIOS ? 'auto' : 'box-none'}>
+            {isMe ? (
+              <>
+                <Button
+                  testID="profileHeaderEditProfileButton"
+                  size="small"
+                  color="secondary"
+                  variant="solid"
+                  onPress={editProfileControl.open}
+                  label={_(msg`Edit profile`)}
+                  style={[a.rounded_full]}>
+                  <ButtonText>
+                    <Trans>Edit Profile</Trans>
+                  </ButtonText>
+                </Button>
+                <EditProfileDialog
                   profile={profile}
-                  moderationOpts={moderationOpts}
+                  control={editProfileControl}
                 />
-              )}
-              {hasSession && <MessageProfileButton profile={profile} />}
-
-              <Button
-                testID={profile.viewer?.following ? 'unfollowBtn' : 'followBtn'}
-                size="small"
-                color={profile.viewer?.following ? 'secondary' : 'primary'}
-                variant="solid"
-                label={
-                  profile.viewer?.following
-                    ? _(msg`Unfollow ${profile.handle}`)
-                    : _(msg`Follow ${profile.handle}`)
-                }
-                onPress={
-                  profile.viewer?.following ? onPressUnfollow : onPressFollow
-                }
-                style={[a.rounded_full]}>
-                {!profile.viewer?.following && (
-                  <ButtonIcon position="left" icon={Plus} />
+              </>
+            ) : profile.viewer?.blocking ? (
+              profile.viewer?.blockingByList ? null : (
+                <Button
+                  testID="unblockBtn"
+                  size="small"
+                  color="secondary"
+                  variant="solid"
+                  label={_(msg`Unblock`)}
+                  disabled={!hasSession}
+                  onPress={() => unblockPromptControl.open()}
+                  style={[a.rounded_full]}>
+                  <ButtonText>
+                    <Trans context="action">Unblock</Trans>
+                  </ButtonText>
+                </Button>
+              )
+            ) : !profile.viewer?.blockedBy ? (
+              <>
+                {hasSession && subscriptionsAllowed && (
+                  <SubscribeProfileButton
+                    profile={profile}
+                    moderationOpts={moderationOpts}
+                  />
                 )}
-                <ButtonText>
-                  {profile.viewer?.following ? (
-                    <Trans>Following</Trans>
-                  ) : profile.viewer?.followedBy ? (
-                    <Trans>Follow Back</Trans>
-                  ) : (
-                    <Trans>Follow</Trans>
+                {hasSession && <MessageProfileButton profile={profile} />}
+
+                <Button
+                  testID={
+                    profile.viewer?.following ? 'unfollowBtn' : 'followBtn'
+                  }
+                  size="small"
+                  color={profile.viewer?.following ? 'secondary' : 'primary'}
+                  variant="solid"
+                  label={
+                    profile.viewer?.following
+                      ? _(msg`Unfollow ${profile.handle}`)
+                      : _(msg`Follow ${profile.handle}`)
+                  }
+                  onPress={
+                    profile.viewer?.following ? onPressUnfollow : onPressFollow
+                  }
+                  style={[a.rounded_full]}>
+                  {!profile.viewer?.following && (
+                    <ButtonIcon position="left" icon={Plus} />
                   )}
-                </ButtonText>
-              </Button>
-            </>
-          ) : null}
-          <ProfileMenu profile={profile} />
-        </View>
-        <View
-          style={[a.flex_col, a.gap_xs, a.pb_sm, live ? a.pt_sm : a.pt_2xs]}>
-          <View style={[a.flex_row, a.align_center, a.gap_xs, a.flex_1]}>
-            <Text
-              emoji
-              testID="profileHeaderDisplayName"
-              style={[
-                t.atoms.text,
-                gtMobile ? a.text_4xl : a.text_3xl,
-                a.self_start,
-                a.font_heavy,
-                a.leading_tight,
-              ]}>
-              {sanitizeDisplayName(
-                profile.displayName || sanitizeHandle(profile.handle),
-                moderation.ui('displayName'),
-              )}
-              <View
+                  <ButtonText>
+                    {profile.viewer?.following ? (
+                      <Trans>Following</Trans>
+                    ) : profile.viewer?.followedBy ? (
+                      <Trans>Follow back</Trans>
+                    ) : (
+                      <Trans>Follow</Trans>
+                    )}
+                  </ButtonText>
+                </Button>
+              </>
+            ) : null}
+            <ProfileMenu profile={profile} />
+          </View>
+          <View
+            style={[a.flex_col, a.gap_xs, a.pb_sm, live ? a.pt_sm : a.pt_2xs]}>
+            <View style={[a.flex_row, a.align_center, a.gap_xs, a.flex_1]}>
+              <Text
+                emoji
+                testID="profileHeaderDisplayName"
                 style={[
-                  a.pl_xs,
-                  {
-                    marginTop: platform({ios: 2}),
-                  },
+                  t.atoms.text,
+                  gtMobile ? a.text_4xl : a.text_3xl,
+                  a.self_start,
+                  a.font_heavy,
+                  a.leading_tight,
                 ]}>
-                <VerificationCheckButton profile={profile} size="lg" />
-              </View>
-            </Text>
+                {sanitizeDisplayName(
+                  profile.displayName || sanitizeHandle(profile.handle),
+                  moderation.ui('displayName'),
+                )}
+                <View
+                  style={[
+                    a.pl_xs,
+                    {
+                      marginTop: platform({ios: 2}),
+                    },
+                  ]}>
+                  <VerificationCheckButton profile={profile} size="lg" />
+                </View>
+              </Text>
+            </View>
+            <ProfileHeaderHandle profile={profile} />
           </View>
-          <ProfileHeaderHandle profile={profile} />
-        </View>
-        {!isPlaceholderProfile && !isBlockedUser && (
-          <View style={a.gap_md}>
-            <ProfileHeaderMetrics profile={profile} />
-            {descriptionRT && !moderation.ui('profileView').blur ? (
-              <View pointerEvents="auto">
-                <RichText
-                  testID="profileHeaderDescription"
-                  style={[a.text_md]}
-                  numberOfLines={15}
-                  value={descriptionRT}
-                  enableTags
-                  authorHandle={profile.handle}
-                />
-              </View>
-            ) : undefined}
-
-            {!isMe &&
-              !isBlockedUser &&
-              shouldShowKnownFollowers(profile.viewer?.knownFollowers) && (
-                <View style={[a.flex_row, a.align_center, a.gap_sm]}>
-                  <KnownFollowers
-                    profile={profile}
-                    moderationOpts={moderationOpts}
+          {!isPlaceholderProfile && !isBlockedUser && (
+            <View style={a.gap_md}>
+              <ProfileHeaderMetrics profile={profile} />
+              {descriptionRT && !moderation.ui('profileView').blur ? (
+                <View pointerEvents="auto">
+                  <RichText
+                    testID="profileHeaderDescription"
+                    style={[a.text_md]}
+                    numberOfLines={15}
+                    value={descriptionRT}
+                    enableTags
+                    authorHandle={profile.handle}
                   />
                 </View>
-              )}
-          </View>
-        )}
-      </View>
-      <Prompt.Basic
-        control={unblockPromptControl}
-        title={_(msg`Unblock Account?`)}
-        description={_(
-          msg`The account will be able to interact with you after unblocking.`,
-        )}
-        onConfirm={unblockAccount}
-        confirmButtonCta={
-          profile.viewer?.blocking ? _(msg`Unblock`) : _(msg`Block`)
-        }
-        confirmButtonColor="negative"
+              ) : undefined}
+
+              {!isMe &&
+                !isBlockedUser &&
+                shouldShowKnownFollowers(profile.viewer?.knownFollowers) && (
+                  <View style={[a.flex_row, a.align_center, a.gap_sm]}>
+                    <KnownFollowers
+                      profile={profile}
+                      moderationOpts={moderationOpts}
+                    />
+                  </View>
+                )}
+            </View>
+          )}
+        </View>
+
+        <Prompt.Basic
+          control={unblockPromptControl}
+          title={_(msg`Unblock Account?`)}
+          description={_(
+            msg`The account will be able to interact with you after unblocking.`,
+          )}
+          onConfirm={unblockAccount}
+          confirmButtonCta={
+            profile.viewer?.blocking ? _(msg`Unblock`) : _(msg`Block`)
+          }
+          confirmButtonColor="negative"
+        />
+      </ProfileHeaderShell>
+
+      <AnimatedProfileHeaderSuggestedFollows
+        isExpanded={showSuggestedFollows}
+        actorDid={profile.did}
       />
-    </ProfileHeaderShell>
+    </>
   )
 }
+
 ProfileHeaderStandard = memo(ProfileHeaderStandard)
 export {ProfileHeaderStandard}
diff --git a/src/screens/Profile/Header/Shell.tsx b/src/screens/Profile/Header/Shell.tsx
index 167be0aa8..cff0a707c 100644
--- a/src/screens/Profile/Header/Shell.tsx
+++ b/src/screens/Profile/Header/Shell.tsx
@@ -211,7 +211,7 @@ let ProfileHeaderShell = ({
 
       {!isPlaceholderProfile && (
         <View
-          style={[a.px_lg, a.py_xs]}
+          style={[a.px_lg, a.pt_xs, a.pb_sm]}
           pointerEvents={isIOS ? 'auto' : 'box-none'}>
           {isMe ? (
             <LabelsOnMe type="account" labels={profile.labels} />
diff --git a/src/screens/Profile/Header/SuggestedFollows.tsx b/src/screens/Profile/Header/SuggestedFollows.tsx
new file mode 100644
index 000000000..d005d888e
--- /dev/null
+++ b/src/screens/Profile/Header/SuggestedFollows.tsx
@@ -0,0 +1,45 @@
+import {AccordionAnimation} from '#/lib/custom-animations/AccordionAnimation'
+import {useGate} from '#/lib/statsig/statsig'
+import {isAndroid} from '#/platform/detection'
+import {useSuggestedFollowsByActorQuery} from '#/state/queries/suggested-follows'
+import {ProfileGrid} from '#/components/FeedInterstitials'
+
+export function ProfileHeaderSuggestedFollows({actorDid}: {actorDid: string}) {
+  const {isLoading, data, error} = useSuggestedFollowsByActorQuery({
+    did: actorDid,
+  })
+
+  return (
+    <ProfileGrid
+      isSuggestionsLoading={isLoading}
+      profiles={data?.suggestions ?? []}
+      recId={data?.recId}
+      error={error}
+      viewContext="profileHeader"
+    />
+  )
+}
+
+export function AnimatedProfileHeaderSuggestedFollows({
+  isExpanded,
+  actorDid,
+}: {
+  isExpanded: boolean
+  actorDid: string
+}) {
+  const gate = useGate()
+  if (!gate('post_follow_profile_suggested_accounts')) return null
+
+  /* NOTE (caidanw):
+   * Android does not work well with this feature yet.
+   * This issue stems from Android not allowing dragging on clickable elements in the profile header.
+   * Blocking the ability to scroll on Android is too much of a trade-off for now.
+   **/
+  if (isAndroid) return null
+
+  return (
+    <AccordionAnimation isExpanded={isExpanded}>
+      <ProfileHeaderSuggestedFollows actorDid={actorDid} />
+    </AccordionAnimation>
+  )
+}
diff --git a/src/screens/Settings/AppIconSettings/index.tsx b/src/screens/Settings/AppIconSettings/index.tsx
index 799873c2d..953ae2e60 100644
--- a/src/screens/Settings/AppIconSettings/index.tsx
+++ b/src/screens/Settings/AppIconSettings/index.tsx
@@ -28,7 +28,7 @@ export function AppIconSettingsScreen({}: Props) {
     getAppIconName(DynamicAppIcon.getAppIcon()),
   )
 
-  const onSetAppIcon = (icon: string) => {
+  const onSetAppIcon = (icon: DynamicAppIcon.IconName) => {
     if (isAndroid) {
       const next =
         sets.defaults.find(i => i.id === icon) ??
@@ -37,7 +37,7 @@ export function AppIconSettingsScreen({}: Props) {
         next
           ? _(msg`Change app icon to "${next.name}"`)
           : _(msg`Change app icon`),
-        // to determine - can we stop this happening? -sfn
+        // unfortunately necessary -sfn
         _(msg`The app will be restarted`),
         [
           {
@@ -119,7 +119,7 @@ export function AppIconSettingsScreen({}: Props) {
   )
 }
 
-function setAppIcon(icon: string) {
+function setAppIcon(icon: DynamicAppIcon.IconName) {
   if (icon === 'default_light') {
     return getAppIconName(DynamicAppIcon.setAppIcon(null))
   } else {
@@ -127,11 +127,11 @@ function setAppIcon(icon: string) {
   }
 }
 
-function getAppIconName(icon: string | false) {
+function getAppIconName(icon: string | false): DynamicAppIcon.IconName {
   if (!icon || icon === 'DEFAULT') {
     return 'default_light'
   } else {
-    return icon
+    return icon as DynamicAppIcon.IconName
   }
 }
 
@@ -143,8 +143,8 @@ function Group({
 }: {
   children: React.ReactNode
   label: string
-  value: string
-  onChange: (value: string) => void
+  value: DynamicAppIcon.IconName
+  onChange: (value: DynamicAppIcon.IconName) => void
 }) {
   return (
     <Toggle.Group
@@ -153,7 +153,7 @@ function Group({
       values={[value]}
       maxSelections={1}
       onChange={vals => {
-        if (vals[0]) onChange(vals[0])
+        if (vals[0]) onChange(vals[0] as DynamicAppIcon.IconName)
       }}>
       <View style={[a.flex_1, a.rounded_md, a.overflow_hidden]}>
         {children}
diff --git a/src/screens/Settings/AppIconSettings/types.ts b/src/screens/Settings/AppIconSettings/types.ts
index 5010f6f02..02c2791dc 100644
--- a/src/screens/Settings/AppIconSettings/types.ts
+++ b/src/screens/Settings/AppIconSettings/types.ts
@@ -1,7 +1,8 @@
-import {ImageSourcePropType} from 'react-native'
+import {type ImageSourcePropType} from 'react-native'
+import type * as DynamicAppIcon from '@mozzius/expo-dynamic-app-icon'
 
 export type AppIconSet = {
-  id: string
+  id: DynamicAppIcon.IconName
   name: string
   iosImage: () => ImageSourcePropType
   androidImage: () => ImageSourcePropType
diff --git a/src/screens/Settings/AppIconSettings/useAppIconSets.ts b/src/screens/Settings/AppIconSettings/useAppIconSets.ts
index fd3caeb30..f7d191f77 100644
--- a/src/screens/Settings/AppIconSettings/useAppIconSets.ts
+++ b/src/screens/Settings/AppIconSettings/useAppIconSets.ts
@@ -37,6 +37,16 @@ export function useAppIconSets() {
           )
         },
       },
+      {
+        id: 'next',
+        name: _(msg({context: 'Name of app icon variant', message: 'Next'})),
+        iosImage: () => {
+          return require(`../../../../assets/app-icons/icon_default_next.png`)
+        },
+        androidImage: () => {
+          return require(`../../../../assets/app-icons/icon_default_next.png`)
+        },
+      },
     ] satisfies AppIconSet[]
 
     /**
diff --git a/src/screens/Settings/AppearanceSettings.tsx b/src/screens/Settings/AppearanceSettings.tsx
index 492d6d172..5d597ff8e 100644
--- a/src/screens/Settings/AppearanceSettings.tsx
+++ b/src/screens/Settings/AppearanceSettings.tsx
@@ -12,7 +12,6 @@ import {
   type CommonNavigatorParams,
   type NativeStackScreenProps,
 } from '#/lib/routes/types'
-import {useGate} from '#/lib/statsig/statsig'
 import {isNative} from '#/platform/detection'
 import {useSetThemePrefs, useThemePrefs} from '#/state/shell'
 import {SettingsListItem as AppIconSettingsListItem} from '#/screens/Settings/AppIconSettings/SettingsListItem'
@@ -32,7 +31,6 @@ type Props = NativeStackScreenProps<CommonNavigatorParams, 'AppearanceSettings'>
 export function AppearanceSettingsScreen({}: Props) {
   const {_} = useLingui()
   const {fonts} = useAlf()
-  const gate = useGate()
 
   const {colorMode, darkTheme} = useThemePrefs()
   const {setColorMode, setDarkTheme} = useSetThemePrefs()
@@ -180,7 +178,7 @@ export function AppearanceSettingsScreen({}: Props) {
                 onChange={onChangeFontScale}
               />
 
-              {isNative && IS_INTERNAL && gate('debug_subscriptions') && (
+              {isNative && IS_INTERNAL && (
                 <>
                   <SettingsList.Divider />
                   <AppIconSettingsListItem />
diff --git a/src/screens/Signup/StepHandle/index.tsx b/src/screens/Signup/StepHandle/index.tsx
index aaab435ae..5bf6b2269 100644
--- a/src/screens/Signup/StepHandle/index.tsx
+++ b/src/screens/Signup/StepHandle/index.tsx
@@ -9,7 +9,6 @@ import Animated, {
 import {msg, Plural, Trans} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 
-import {useGate} from '#/lib/statsig/statsig'
 import {
   createFullHandle,
   MAX_SERVICE_HANDLE_LENGTH,
@@ -28,14 +27,12 @@ import {useThrottledValue} from '#/components/hooks/useThrottledValue'
 import {At_Stroke2_Corner0_Rounded as AtIcon} from '#/components/icons/At'
 import {Check_Stroke2_Corner0_Rounded as CheckIcon} from '#/components/icons/Check'
 import {Text} from '#/components/Typography'
-import {IS_INTERNAL} from '#/env'
 import {BackNextButtons} from '../BackNextButtons'
 import {HandleSuggestions} from './HandleSuggestions'
 
 export function StepHandle() {
   const {_} = useLingui()
   const t = useTheme()
-  const gate = useGate()
   const {state, dispatch} = useSignupContext()
   const [draftValue, setDraftValue] = useState(state.handle)
   const isNextLoading = useThrottledValue(state.isLoading, 500)
@@ -193,8 +190,7 @@ export function StepHandle() {
                   </RequirementText>
                 </Requirement>
                 {isHandleAvailable.suggestions &&
-                  isHandleAvailable.suggestions.length > 0 &&
-                  (gate('handle_suggestions') || IS_INTERNAL) && (
+                  isHandleAvailable.suggestions.length > 0 && (
                     <HandleSuggestions
                       suggestions={isHandleAvailable.suggestions}
                       onSelect={suggestion => {