import Button from '@components/Button' import Icon from '@components/Icon' import { useNavigation } from '@react-navigation/native' import apiGeneral from '@utils/api/general' import browserPackage from '@utils/helpers/browserPackage' import { featureCheck } from '@utils/helpers/featureCheck' import { TabMeStackNavigationProp } from '@utils/navigation/navigators' import { queryClient } from '@utils/queryHooks' import { redirectUri, useAppsMutation } from '@utils/queryHooks/apps' import { useInstanceQuery } from '@utils/queryHooks/instance' import { StorageAccount } from '@utils/storage/account' import { generateAccountKey, getGlobalStorage, setAccount, setAccountStorage, setGlobalStorage } from '@utils/storage/actions' import { StyleConstants } from '@utils/styles/constants' import { useTheme } from '@utils/styles/ThemeManager' import * as AuthSession from 'expo-auth-session' import * as WebBrowser from 'expo-web-browser' import { debounce } from 'lodash' import React, { RefObject, useCallback, useState } from 'react' import { Trans, useTranslation } from 'react-i18next' import { Alert, Image, KeyboardAvoidingView, Platform, TextInput, View } from 'react-native' import { ScrollView } from 'react-native-gesture-handler' import parse from 'url-parse' import CustomText from '../Text' export interface Props { scrollViewRef?: RefObject disableHeaderImage?: boolean goBack?: boolean } const ComponentInstance: React.FC = ({ scrollViewRef, disableHeaderImage, goBack = false }) => { const { t } = useTranslation(['common', 'componentInstance']) const { colors, mode } = useTheme() const navigation = useNavigation>() const [domain, setDomain] = useState('') const [errorCode, setErrorCode] = useState(null) const whitelisted: boolean = !!domain.length && !!errorCode && !!(parse(`https://${domain}/`).hostname === domain) && errorCode === 401 const instanceQuery = useInstanceQuery({ domain, options: { enabled: !!domain, retry: false, onError: err => { if (err.status) { setErrorCode(err.status) } } } }) const appsMutation = useAppsMutation({ retry: false, onSuccess: async (data, variables) => { const scopes = featureCheck('deprecate_auth_follow') ? ['read', 'write', 'push'] : ['read', 'write', 'follow', 'push'] const clientId = data.client_id const clientSecret = data.client_secret const discovery = { authorizationEndpoint: `https://${domain}/oauth/authorize` } const request = new AuthSession.AuthRequest({ clientId, clientSecret, scopes, redirectUri }) await request.makeAuthUrlAsync(discovery) const promptResult = await request.promptAsync(discovery, await browserPackage()) if (promptResult?.type === 'success') { const { accessToken } = await AuthSession.exchangeCodeAsync( { clientId, clientSecret, scopes, redirectUri, code: promptResult.params.code, extraParams: { client_id: clientId, client_secret: clientSecret, grant_type: 'authorization_code', ...(request.codeVerifier && { code_verifier: request.codeVerifier }) } }, { tokenEndpoint: `https://${variables.domain}/oauth/token` } ) queryClient.clear() const { body: { id, acct, avatar_static } } = await apiGeneral({ method: 'get', domain, url: `api/v1/accounts/verify_credentials`, headers: { Authorization: `Bearer ${accessToken}` } }) const accounts = getGlobalStorage.object('accounts') const accountKey = generateAccountKey({ domain, id }) const account = accounts?.find(account => account === accountKey) const accountDetails: StorageAccount = { 'auth.clientId': clientId, 'auth.clientSecret': clientSecret, 'auth.token': accessToken, 'auth.domain': domain, 'auth.account.id': id, 'auth.account.acct': acct, 'auth.account.domain': (instanceQuery.data as Mastodon.Instance_V2)?.domain || instanceQuery.data?.account_domain || ((instanceQuery.data as Mastodon.Instance_V1)?.uri ? parse((instanceQuery.data as Mastodon.Instance_V1).uri).hostname : undefined) || (instanceQuery.data as Mastodon.Instance_V1)?.uri, 'auth.account.avatar_static': avatar_static, version: instanceQuery.data?.version || '0', preferences: undefined, notifications: { follow: true, follow_request: true, favourite: true, reblog: true, mention: true, poll: true, status: true, update: true, 'admin.sign_up': true, 'admin.report': true }, push: { global: false, decode: false, alerts: { follow: true, follow_request: true, favourite: true, reblog: true, mention: true, poll: true, status: true, update: true, 'admin.sign_up': false, 'admin.report': false }, key: Math.random().toString(36).slice(2, 12) }, page_local: { showBoosts: true, showReplies: true }, page_me: { followedTags: { shown: false }, lists: { shown: false }, announcements: { shown: false, unread: 0 } }, drafts: [], emojis_frequent: [] } setAccountStorage( Object.keys(accountDetails).map((key: keyof StorageAccount) => ({ key, value: accountDetails[key] })), accountKey ) if (!account) { setGlobalStorage('accounts', accounts?.concat([accountKey])) } setAccount(accountKey) goBack && navigation.goBack() } } }) const processUpdate = useCallback(() => { if (domain) { const accounts = getGlobalStorage.object('accounts') if (accounts?.filter(account => account.startsWith(`${domain}/`)).length) { Alert.alert( t('componentInstance:update.alert.title'), t('componentInstance:update.alert.message'), [ { text: t('common:buttons.cancel'), style: 'cancel' }, { text: t('common:buttons.continue'), onPress: () => appsMutation.mutate({ domain }) } ] ) } else { appsMutation.mutate({ domain }) } } }, [domain]) return ( {!disableHeaderImage ? ( ) : null} { setDomain(text.replace(/^http(s)?\:\/\//i, '')) setErrorCode(null) }, 1000, { trailing: true } )} autoCapitalize='none' clearButtonMode='never' keyboardType='url' textContentType='URL' onSubmitEditing={({ nativeEvent: { text } }) => { if ( text === domain && instanceQuery.isSuccess && instanceQuery.data && // @ts-ignore (instanceQuery.data.domain || instanceQuery.data.uri) ) { processUpdate() } }} placeholder={' ' + t('componentInstance:server.textInput.placeholder')} placeholderTextColor={colors.secondary} returnKeyType='go' keyboardAppearance={mode} {...(scrollViewRef && { onFocus: () => setTimeout(() => scrollViewRef.current?.scrollTo({ y: 0, animated: true }), 150) })} autoCorrect={false} spellCheck={false} />