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/Moderation/index.tsx131
-rw-r--r--src/screens/Settings/AccountSettings.tsx2
-rw-r--r--src/screens/Settings/components/DisableEmail2FADialog.tsx201
-rw-r--r--src/screens/Settings/components/Email2FAToggle.tsx2
-rw-r--r--src/screens/Settings/components/ExportCarDialog.tsx110
5 files changed, 313 insertions, 133 deletions
diff --git a/src/screens/Moderation/index.tsx b/src/screens/Moderation/index.tsx
index d5a2daffd..5f340cd56 100644
--- a/src/screens/Moderation/index.tsx
+++ b/src/screens/Moderation/index.tsx
@@ -1,13 +1,11 @@
 import React from 'react'
 import {Linking, View} from 'react-native'
 import {useSafeAreaFrame} from 'react-native-safe-area-context'
-import {ComAtprotoLabelDefs} from '@atproto/api'
 import {LABELS} from '@atproto/api'
 import {msg, Trans} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 import {useFocusEffect} from '@react-navigation/native'
 
-import {IS_INTERNAL} from '#/lib/app-info'
 import {getLabelingServiceTitle} from '#/lib/moderation'
 import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
 import {logger} from '#/logger'
@@ -18,11 +16,6 @@ import {
   UsePreferencesQueryResponse,
   usePreferencesSetAdultContentMutation,
 } from '#/state/queries/preferences'
-import {
-  useProfileQuery,
-  useProfileUpdateMutation,
-} from '#/state/queries/profile'
-import {useSession} from '#/state/session'
 import {isNonConfigurableModerationAuthority} from '#/state/session/additional-moderation-authorities'
 import {useSetMinimalShellMode} from '#/state/shell'
 import {ViewHeader} from '#/view/com/util/ViewHeader'
@@ -469,131 +462,7 @@ export function ModerationScreenInner({
           })}
         </View>
       )}
-
-      {!IS_INTERNAL && (
-        <>
-          <Text
-            style={[
-              a.text_md,
-              a.font_bold,
-              a.pt_2xl,
-              a.pb_md,
-              t.atoms.text_contrast_high,
-            ]}>
-            <Trans>Logged-out visibility</Trans>
-          </Text>
-
-          <PwiOptOut />
-        </>
-      )}
-
       <View style={{height: 200}} />
     </ScrollView>
   )
 }
-
-function PwiOptOut() {
-  const t = useTheme()
-  const {_} = useLingui()
-  const {currentAccount} = useSession()
-  const {data: profile} = useProfileQuery({did: currentAccount?.did})
-  const updateProfile = useProfileUpdateMutation()
-
-  const isOptedOut =
-    profile?.labels?.some(l => l.val === '!no-unauthenticated') || false
-  const canToggle = profile && !updateProfile.isPending
-
-  const onToggleOptOut = React.useCallback(() => {
-    if (!profile) {
-      return
-    }
-    let wasAdded = false
-    updateProfile.mutate({
-      profile,
-      updates: existing => {
-        // create labels attr if needed
-        existing.labels = ComAtprotoLabelDefs.isSelfLabels(existing.labels)
-          ? existing.labels
-          : {
-              $type: 'com.atproto.label.defs#selfLabels',
-              values: [],
-            }
-
-        // toggle the label
-        const hasLabel = existing.labels.values.some(
-          l => l.val === '!no-unauthenticated',
-        )
-        if (hasLabel) {
-          wasAdded = false
-          existing.labels.values = existing.labels.values.filter(
-            l => l.val !== '!no-unauthenticated',
-          )
-        } else {
-          wasAdded = true
-          existing.labels.values.push({val: '!no-unauthenticated'})
-        }
-
-        // delete if no longer needed
-        if (existing.labels.values.length === 0) {
-          delete existing.labels
-        }
-        return existing
-      },
-      checkCommitted: res => {
-        const exists = !!res.data.labels?.some(
-          l => l.val === '!no-unauthenticated',
-        )
-        return exists === wasAdded
-      },
-    })
-  }, [updateProfile, profile])
-
-  return (
-    <View style={[a.pt_sm]}>
-      <View style={[a.flex_row, a.align_center, a.justify_between, a.gap_lg]}>
-        <Toggle.Item
-          disabled={!canToggle}
-          value={isOptedOut}
-          onChange={onToggleOptOut}
-          name="logged_out_visibility"
-          style={a.flex_1}
-          label={_(
-            msg`Discourage apps from showing my account to logged-out users`,
-          )}>
-          <Toggle.Switch />
-          <Toggle.LabelText style={[a.text_md, a.flex_1]}>
-            <Trans>
-              Discourage apps from showing my account to logged-out users
-            </Trans>
-          </Toggle.LabelText>
-        </Toggle.Item>
-
-        {updateProfile.isPending && <Loader />}
-      </View>
-
-      <View style={[a.pt_md, a.gap_md, {paddingLeft: 38}]}>
-        <Text style={[a.leading_snug, t.atoms.text_contrast_high]}>
-          <Trans>
-            Bluesky will not show your profile and posts to logged-out users.
-            Other apps may not honor this request. This does not make your
-            account private.
-          </Trans>
-        </Text>
-        <Text style={[a.font_bold, a.leading_snug, t.atoms.text_contrast_high]}>
-          <Trans>
-            Note: Bluesky is an open and public network. This setting only
-            limits the visibility of your content on the Bluesky app and
-            website, and other apps may not respect this setting. Your content
-            may still be shown to logged-out users by other apps and websites.
-          </Trans>
-        </Text>
-
-        <InlineLinkText
-          label={_(msg`Learn more about what is public on Bluesky.`)}
-          to="https://blueskyweb.zendesk.com/hc/en-us/articles/15835264007693-Data-Privacy">
-          <Trans>Learn more about what is public on Bluesky.</Trans>
-        </InlineLinkText>
-      </View>
-    </View>
-  )
-}
diff --git a/src/screens/Settings/AccountSettings.tsx b/src/screens/Settings/AccountSettings.tsx
index f34810a68..35c5f3aa0 100644
--- a/src/screens/Settings/AccountSettings.tsx
+++ b/src/screens/Settings/AccountSettings.tsx
@@ -6,7 +6,6 @@ import {NativeStackScreenProps} from '@react-navigation/native-stack'
 import {CommonNavigatorParams} from '#/lib/routes/types'
 import {useModalControls} from '#/state/modals'
 import {useSession} from '#/state/session'
-import {ExportCarDialog} from '#/view/screens/Settings/ExportCarDialog'
 import * as SettingsList from '#/screens/Settings/components/SettingsList'
 import {atoms as a, useTheme} from '#/alf'
 import {useDialogControl} from '#/components/Dialog'
@@ -24,6 +23,7 @@ import {Verified_Stroke2_Corner2_Rounded as VerifiedIcon} from '#/components/ico
 import * as Layout from '#/components/Layout'
 import {ChangeHandleDialog} from './components/ChangeHandleDialog'
 import {DeactivateAccountDialog} from './components/DeactivateAccountDialog'
+import {ExportCarDialog} from './components/ExportCarDialog'
 
 type Props = NativeStackScreenProps<CommonNavigatorParams, 'AccountSettings'>
 export function AccountSettingsScreen({}: Props) {
diff --git a/src/screens/Settings/components/DisableEmail2FADialog.tsx b/src/screens/Settings/components/DisableEmail2FADialog.tsx
new file mode 100644
index 000000000..1378759b0
--- /dev/null
+++ b/src/screens/Settings/components/DisableEmail2FADialog.tsx
@@ -0,0 +1,201 @@
+import React, {useState} from 'react'
+import {View} from 'react-native'
+import {msg, Trans} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+
+import {cleanError} from '#/lib/strings/errors'
+import {isNative} from '#/platform/detection'
+import {useAgent, useSession} from '#/state/session'
+import {ErrorMessage} from '#/view/com/util/error/ErrorMessage'
+import * as Toast from '#/view/com/util/Toast'
+import {atoms as a, useBreakpoints, useTheme} from '#/alf'
+import {Button, ButtonIcon, ButtonText} from '#/components/Button'
+import * as Dialog from '#/components/Dialog'
+import * as TextField from '#/components/forms/TextField'
+import {Lock_Stroke2_Corner0_Rounded as Lock} from '#/components/icons/Lock'
+import {Loader} from '#/components/Loader'
+import {P, Text} from '#/components/Typography'
+
+enum Stages {
+  Email,
+  ConfirmCode,
+}
+
+export function DisableEmail2FADialog({
+  control,
+}: {
+  control: Dialog.DialogOuterProps['control']
+}) {
+  const {_} = useLingui()
+  const t = useTheme()
+  const {gtMobile} = useBreakpoints()
+  const {currentAccount} = useSession()
+  const agent = useAgent()
+
+  const [stage, setStage] = useState<Stages>(Stages.Email)
+  const [confirmationCode, setConfirmationCode] = useState<string>('')
+  const [isProcessing, setIsProcessing] = useState<boolean>(false)
+  const [error, setError] = useState<string>('')
+
+  const onSendEmail = async () => {
+    setError('')
+    setIsProcessing(true)
+    try {
+      await agent.com.atproto.server.requestEmailUpdate()
+      setStage(Stages.ConfirmCode)
+    } catch (e) {
+      setError(cleanError(String(e)))
+    } finally {
+      setIsProcessing(false)
+    }
+  }
+
+  const onConfirmDisable = async () => {
+    setError('')
+    setIsProcessing(true)
+    try {
+      if (currentAccount?.email) {
+        await agent.com.atproto.server.updateEmail({
+          email: currentAccount!.email,
+          token: confirmationCode.trim(),
+          emailAuthFactor: false,
+        })
+        await agent.resumeSession(agent.session!)
+        Toast.show(_(msg`Email 2FA disabled`))
+      }
+      control.close()
+    } catch (e) {
+      const errMsg = String(e)
+      if (errMsg.includes('Token is invalid')) {
+        setError(_(msg`Invalid 2FA confirmation code.`))
+      } else {
+        setError(cleanError(errMsg))
+      }
+    } finally {
+      setIsProcessing(false)
+    }
+  }
+
+  return (
+    <Dialog.Outer control={control}>
+      <Dialog.Handle />
+      <Dialog.ScrollableInner
+        accessibilityDescribedBy="dialog-description"
+        accessibilityLabelledBy="dialog-title">
+        <View style={[a.relative, a.gap_md, a.w_full]}>
+          <Text
+            nativeID="dialog-title"
+            style={[a.text_2xl, a.font_bold, t.atoms.text]}>
+            <Trans>Disable Email 2FA</Trans>
+          </Text>
+          <P nativeID="dialog-description">
+            {stage === Stages.ConfirmCode ? (
+              <Trans>
+                An email has been sent to{' '}
+                {currentAccount?.email || '(no email)'}. It includes a
+                confirmation code which you can enter below.
+              </Trans>
+            ) : (
+              <Trans>
+                To disable the email 2FA method, please verify your access to
+                the email address.
+              </Trans>
+            )}
+          </P>
+
+          {error ? <ErrorMessage message={error} /> : undefined}
+
+          {stage === Stages.Email ? (
+            <View
+              style={[
+                a.gap_sm,
+                gtMobile && [a.flex_row, a.justify_end, a.gap_md],
+              ]}>
+              <Button
+                testID="sendEmailButton"
+                variant="solid"
+                color="primary"
+                size={gtMobile ? 'small' : 'large'}
+                onPress={onSendEmail}
+                label={_(msg`Send verification email`)}
+                disabled={isProcessing}>
+                <ButtonText>
+                  <Trans>Send verification email</Trans>
+                </ButtonText>
+                {isProcessing && <ButtonIcon icon={Loader} />}
+              </Button>
+              <Button
+                testID="haveCodeButton"
+                variant="ghost"
+                color="primary"
+                size={gtMobile ? 'small' : 'large'}
+                onPress={() => setStage(Stages.ConfirmCode)}
+                label={_(msg`I have a code`)}
+                disabled={isProcessing}>
+                <ButtonText>
+                  <Trans>I have a code</Trans>
+                </ButtonText>
+              </Button>
+            </View>
+          ) : stage === Stages.ConfirmCode ? (
+            <View>
+              <View style={[a.mb_md]}>
+                <TextField.LabelText>
+                  <Trans>Confirmation code</Trans>
+                </TextField.LabelText>
+                <TextField.Root>
+                  <TextField.Icon icon={Lock} />
+                  <Dialog.Input
+                    testID="confirmationCode"
+                    label={_(msg`Confirmation code`)}
+                    autoCapitalize="none"
+                    autoFocus
+                    autoCorrect={false}
+                    autoComplete="off"
+                    value={confirmationCode}
+                    onChangeText={setConfirmationCode}
+                    onSubmitEditing={onConfirmDisable}
+                    editable={!isProcessing}
+                  />
+                </TextField.Root>
+              </View>
+              <View
+                style={[
+                  a.gap_sm,
+                  gtMobile && [a.flex_row, a.justify_end, a.gap_md],
+                ]}>
+                <Button
+                  testID="resendCodeBtn"
+                  variant="ghost"
+                  color="primary"
+                  size={gtMobile ? 'small' : 'large'}
+                  onPress={onSendEmail}
+                  label={_(msg`Resend email`)}
+                  disabled={isProcessing}>
+                  <ButtonText>
+                    <Trans>Resend email</Trans>
+                  </ButtonText>
+                </Button>
+                <Button
+                  testID="confirmBtn"
+                  variant="solid"
+                  color="primary"
+                  size={gtMobile ? 'small' : 'large'}
+                  onPress={onConfirmDisable}
+                  label={_(msg`Confirm`)}
+                  disabled={isProcessing}>
+                  <ButtonText>
+                    <Trans>Confirm</Trans>
+                  </ButtonText>
+                  {isProcessing && <ButtonIcon icon={Loader} />}
+                </Button>
+              </View>
+            </View>
+          ) : undefined}
+
+          {!gtMobile && isNative && <View style={{height: 40}} />}
+        </View>
+      </Dialog.ScrollableInner>
+    </Dialog.Outer>
+  )
+}
diff --git a/src/screens/Settings/components/Email2FAToggle.tsx b/src/screens/Settings/components/Email2FAToggle.tsx
index 85ae89dea..a74f9fce7 100644
--- a/src/screens/Settings/components/Email2FAToggle.tsx
+++ b/src/screens/Settings/components/Email2FAToggle.tsx
@@ -4,9 +4,9 @@ import {useLingui} from '@lingui/react'
 
 import {useModalControls} from '#/state/modals'
 import {useAgent, useSession} from '#/state/session'
-import {DisableEmail2FADialog} from '#/view/screens/Settings/DisableEmail2FADialog'
 import {useDialogControl} from '#/components/Dialog'
 import * as Prompt from '#/components/Prompt'
+import {DisableEmail2FADialog} from './DisableEmail2FADialog'
 import * as SettingsList from './SettingsList'
 
 export function Email2FAToggle() {
diff --git a/src/screens/Settings/components/ExportCarDialog.tsx b/src/screens/Settings/components/ExportCarDialog.tsx
new file mode 100644
index 000000000..2de3895d3
--- /dev/null
+++ b/src/screens/Settings/components/ExportCarDialog.tsx
@@ -0,0 +1,110 @@
+import React from 'react'
+import {View} from 'react-native'
+import {msg, Trans} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+
+import {saveBytesToDisk} from '#/lib/media/manip'
+import {logger} from '#/logger'
+import {useAgent} from '#/state/session'
+import * as Toast from '#/view/com/util/Toast'
+import {atoms as a, useTheme} from '#/alf'
+import {Button, ButtonIcon, ButtonText} from '#/components/Button'
+import * as Dialog from '#/components/Dialog'
+import {Download_Stroke2_Corner0_Rounded as DownloadIcon} from '#/components/icons/Download'
+import {InlineLinkText} from '#/components/Link'
+import {Loader} from '#/components/Loader'
+import {Text} from '#/components/Typography'
+
+export function ExportCarDialog({
+  control,
+}: {
+  control: Dialog.DialogOuterProps['control']
+}) {
+  const {_} = useLingui()
+  const t = useTheme()
+  const agent = useAgent()
+  const [loading, setLoading] = React.useState(false)
+
+  const download = React.useCallback(async () => {
+    if (!agent.session) {
+      return // shouldnt ever happen
+    }
+    try {
+      setLoading(true)
+      const did = agent.session.did
+      const downloadRes = await agent.com.atproto.sync.getRepo({did})
+      const saveRes = await saveBytesToDisk(
+        'repo.car',
+        downloadRes.data,
+        downloadRes.headers['content-type'],
+      )
+
+      if (saveRes) {
+        Toast.show(_(msg`File saved successfully!`))
+      }
+    } catch (e) {
+      logger.error('Error occurred while downloading CAR file', {message: e})
+      Toast.show(_(msg`Error occurred while saving file`), 'xmark')
+    } finally {
+      setLoading(false)
+      control.close()
+    }
+  }, [_, control, agent])
+
+  return (
+    <Dialog.Outer control={control}>
+      <Dialog.Handle />
+      <Dialog.ScrollableInner
+        accessibilityDescribedBy="dialog-description"
+        accessibilityLabelledBy="dialog-title">
+        <View style={[a.relative, a.gap_lg, a.w_full]}>
+          <Text nativeID="dialog-title" style={[a.text_2xl, a.font_bold]}>
+            <Trans>Export My Data</Trans>
+          </Text>
+          <Text nativeID="dialog-description" style={[a.text_sm]}>
+            <Trans>
+              Your account repository, containing all public data records, can
+              be downloaded as a "CAR" file. This file does not include media
+              embeds, such as images, or your private data, which must be
+              fetched separately.
+            </Trans>
+          </Text>
+
+          <Button
+            variant="solid"
+            color="primary"
+            size="large"
+            label={_(msg`Download CAR file`)}
+            disabled={loading}
+            onPress={download}>
+            <ButtonIcon icon={DownloadIcon} />
+            <ButtonText>
+              <Trans>Download CAR file</Trans>
+            </ButtonText>
+            {loading && <ButtonIcon icon={Loader} />}
+          </Button>
+
+          <Text
+            style={[
+              t.atoms.text_contrast_medium,
+              a.text_sm,
+              a.leading_snug,
+              a.flex_1,
+            ]}>
+            <Trans>
+              This feature is in beta. You can read more about repository
+              exports in{' '}
+              <InlineLinkText
+                label={_(msg`View blogpost for more details`)}
+                to="https://docs.bsky.app/blog/repo-export"
+                style={[a.text_sm]}>
+                this blogpost
+              </InlineLinkText>
+              .
+            </Trans>
+          </Text>
+        </View>
+      </Dialog.ScrollableInner>
+    </Dialog.Outer>
+  )
+}