diff --git a/lib/backup/persist-shared-migrations.js b/lib/backup/persist-shared-migrations.js --- a/lib/backup/persist-shared-migrations.js +++ b/lib/backup/persist-shared-migrations.js @@ -1,7 +1,12 @@ // @flow +import type { ClientDBSyncedMetadataStoreOperation } from '../ops/synced-metadata-store-ops.js'; +import { createReplaceSyncedMetadataOperation } from '../ops/synced-metadata-store-ops.js'; +import { alertTypes, defaultAlertInfo } from '../types/alert-types.js'; import type { DatabaseIdentifier } from '../types/database-identifier-types.js'; import type { StoreOperations } from '../types/store-ops-types.js'; +import { syncedMetadataNames } from '../types/synced-metadata-types.js'; +import { getConfig } from '../utils/config.js'; export type SharedMigrationFunction = ( databaseIdentifier: DatabaseIdentifier, @@ -10,6 +15,40 @@ export type SharedMigrationsManifest = { +[number | string]: SharedMigrationFunction, }; -const sharedMigrations: SharedMigrationsManifest = {}; +const sharedMigrations: SharedMigrationsManifest = { + [96]: (async (databaseIdentifier: DatabaseIdentifier) => { + const { sqliteAPI } = getConfig(); + + const clientStoreToMigrate = + await sqliteAPI.getClientDBStore(databaseIdentifier); + + if (!clientStoreToMigrate.syncedMetadata) { + return {}; + } + + const alertStore = JSON.parse( + clientStoreToMigrate.syncedMetadata[syncedMetadataNames.ALERT_STORE], + ); + const updatedAlertStore = { + ...alertStore, + alertInfos: { + ...alertStore.alertInfos, + [alertTypes.CONNECT_FARCASTER]: defaultAlertInfo, + }, + }; + + const syncedMetadataStoreOperations: $ReadOnlyArray = + [ + createReplaceSyncedMetadataOperation( + syncedMetadataNames.ALERT_STORE, + JSON.stringify(updatedAlertStore), + ), + ]; + + return { + syncedMetadataStoreOperations, + }; + }: SharedMigrationFunction), +}; export { sharedMigrations }; diff --git a/lib/components/farcaster-data-handler.react.js b/lib/components/farcaster-data-handler.react.js --- a/lib/components/farcaster-data-handler.react.js +++ b/lib/components/farcaster-data-handler.react.js @@ -198,8 +198,8 @@ await neynarClient.checkIfCurrentUserFIDIsValid(fid); if (!isCurrentUserFIDValid) { await unlinkFID(); + return; } - return; } if (identityFID) { diff --git a/lib/types/tunnelbroker/user-actions-peer-to-peer-message-types.js b/lib/types/tunnelbroker/user-actions-peer-to-peer-message-types.js --- a/lib/types/tunnelbroker/user-actions-peer-to-peer-message-types.js +++ b/lib/types/tunnelbroker/user-actions-peer-to-peer-message-types.js @@ -70,13 +70,13 @@ export type FarcasterConnectionUpdated = { +type: 'FARCASTER_CONNECTION_UPDATED', +farcasterID: ?string, - +hasDCsToken: ?boolean, + +hasDCsToken: boolean, }; export const farcasterConnectionUpdatedValidator: TInterface = tShape({ type: tString(userActionsP2PMessageTypes.FARCASTER_CONNECTION_UPDATED), farcasterID: t.maybe(t.String), - hasDCsToken: t.maybe(t.Boolean), + hasDCsToken: t.Boolean, }); export type UserActionP2PMessage = diff --git a/lib/utils/farcaster-utils.js b/lib/utils/farcaster-utils.js --- a/lib/utils/farcaster-utils.js +++ b/lib/utils/farcaster-utils.js @@ -21,7 +21,6 @@ const DISABLE_CONNECT_FARCASTER_ALERT = false; const NO_FID_METADATA = 'NONE'; -const NO_DCS_SUPPORT_METADATA = 'NONE'; function useCurrentUserFID(): ?string { // There is a distinction between null & undefined for the fid value. @@ -42,11 +41,7 @@ return currentUserFID; } -function useCurrentUserSupportsDCs(): ?boolean { - // There is a distinction between null & undefined for the fid DCs value. - // If the fid DCs is null this means that the user has decided NOT to set - // a Farcaster DCs association. If the fid DCs is undefined this means that - // the user has not yet been prompted to set a Farcaster DCs association. +function useCurrentUserSupportsDCs(): boolean { const currentUserFIDDCs = useSelector( state => state.syncedMetadataStore.syncedMetadata[ @@ -54,10 +49,6 @@ ] ?? undefined, ); - if (currentUserFIDDCs === NO_DCS_SUPPORT_METADATA) { - return null; - } - return currentUserFIDDCs === 'true'; } @@ -96,16 +87,13 @@ ); } -function useSetLocalCurrentUserSupportsDCs(): (connected: ?boolean) => void { +function useSetLocalCurrentUserSupportsDCs(): (connected: boolean) => void { const dispatch = useDispatch(); const { invalidateCacheForUser } = useUserIdentityCache(); const currentUserID = useSelector(state => state.currentUserInfo?.id); return React.useCallback( (connected: ?boolean) => { - // If we're unsetting the DCs support, we should set it to - // NO_DCS_SUPPORT_METADATA to avoid prompting the user for it again - const connectionStatus = - connected === null ? NO_DCS_SUPPORT_METADATA : String(connected); + const connectionStatus = String(connected); dispatch({ type: setSyncedMetadataEntryActionType, payload: { @@ -152,7 +140,7 @@ async (fid: string) => { await linkFarcasterAccount(fid); setLocalFID(fid); - await broadcastConnectionStatus(fid, null); + await broadcastConnectionStatus(fid, false); }, [linkFarcasterAccount, setLocalFID, broadcastConnectionStatus], ); @@ -199,9 +187,9 @@ return React.useCallback(async () => { await unlinkFarcasterAccount(); - setLocalFID(null); - setLocalDCsSupport(null); - await broadcastConnectionStatus(null, null); + setLocalFID(undefined); + setLocalDCsSupport(false); + await broadcastConnectionStatus(null, false); clearFarcasterThreads(); }, [ unlinkFarcasterAccount, @@ -243,7 +231,7 @@ const currentUserID = useSelector(state => state.currentUserInfo?.id); const userDevices = useSelector(getOwnPeerDevices); return React.useCallback( - async (farcasterID: ?string, hasDCsToken: ?boolean) => { + async (farcasterID: ?string, hasDCsToken: boolean) => { if (!currentUserID) { return; } @@ -304,7 +292,6 @@ export { DISABLE_CONNECT_FARCASTER_ALERT, NO_FID_METADATA, - NO_DCS_SUPPORT_METADATA, useCurrentUserFID, useCurrentUserSupportsDCs, useFarcasterDCsLoaded, diff --git a/lib/utils/push-alerts.js b/lib/utils/push-alerts.js --- a/lib/utils/push-alerts.js +++ b/lib/utils/push-alerts.js @@ -1,8 +1,8 @@ // @flow +import { isDev } from './dev-utils.js'; +import { DISABLE_CONNECT_FARCASTER_ALERT } from './farcaster-utils.js'; import type { AlertInfo } from '../types/alert-types.js'; -import { isDev } from '../utils/dev-utils.js'; -import { DISABLE_CONNECT_FARCASTER_ALERT } from '../utils/farcaster-utils.js'; const msInDay = 24 * 60 * 60 * 1000; @@ -26,7 +26,7 @@ return ( isDev || DISABLE_CONNECT_FARCASTER_ALERT || - fid !== undefined || + fid === null || alertInfo.totalAlerts > 0 ); } diff --git a/native/bottom-sheet/bottom-sheet.react.js b/native/bottom-sheet/bottom-sheet.react.js --- a/native/bottom-sheet/bottom-sheet.react.js +++ b/native/bottom-sheet/bottom-sheet.react.js @@ -52,6 +52,8 @@ backdropComponent={BottomSheetBackdrop} onChange={onChange} enablePanDownToClose={true} + keyboardBehavior="interactive" + keyboardBlurBehavior="restore" > {children} diff --git a/native/components/connect-farcaster-alert-handler.react.js b/native/components/connect-farcaster-alert-handler.react.js --- a/native/components/connect-farcaster-alert-handler.react.js +++ b/native/components/connect-farcaster-alert-handler.react.js @@ -11,10 +11,12 @@ } from 'lib/types/alert-types.js'; import { useCurrentUserFID, + useCurrentUserSupportsDCs, useSetLocalFID, } from 'lib/utils/farcaster-utils.js'; import { shouldSkipConnectFarcasterAlert } from 'lib/utils/push-alerts.js'; import { useDispatch } from 'lib/utils/redux-utils.js'; +import { supportsFarcasterDCs } from 'lib/utils/services-utils.js'; import sleep from 'lib/utils/sleep.js'; import { ConnectFarcasterBottomSheetRouteName } from '../navigation/route-names.js'; @@ -28,6 +30,7 @@ const loggedIn = useIsLoggedInToIdentityAndAuthoritativeKeyserver(); const fid = useCurrentUserFID(); + const currentUserSupportsDCs = useCurrentUserSupportsDCs(); const setLocalFID = useSetLocalFID(); @@ -38,11 +41,16 @@ const dispatch = useDispatch(); React.useEffect(() => { + const shouldShowForDCs = fid && !currentUserSupportsDCs; + const shouldShowForInitialConnection = !fid; + if ( !loggedIn || !isActive || shouldSkipConnectFarcasterAlert(connectFarcasterAlertInfo, fid) || - connectFarcasterAlertInfo.coldStartCount < 2 + connectFarcasterAlertInfo.coldStartCount < 2 || + (!shouldShowForInitialConnection && !shouldShowForDCs) || + !supportsFarcasterDCs ) { return; } @@ -54,7 +62,9 @@ // again. We set it here, rather than in the bottom sheet itself, to avoid // the scenario where the user connects their Farcaster account but we // accidentally overwrite the FID on close and set it to null. - setLocalFID(null); + if (!fid) { + setLocalFID(null); + } navigate(ConnectFarcasterBottomSheetRouteName); const payload: RecordAlertActionPayload = { @@ -69,6 +79,7 @@ })(); }, [ connectFarcasterAlertInfo, + currentUserSupportsDCs, dispatch, fid, isActive, diff --git a/native/components/connect-farcaster-bottom-sheet.react.js b/native/components/connect-farcaster-bottom-sheet.react.js --- a/native/components/connect-farcaster-bottom-sheet.react.js +++ b/native/components/connect-farcaster-bottom-sheet.react.js @@ -6,7 +6,11 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { useIsAppForegrounded } from 'lib/shared/lifecycle-utils.js'; -import { useCurrentUserFID } from 'lib/utils/farcaster-utils.js'; +import { + useCurrentUserFID, + useCurrentUserSupportsDCs, +} from 'lib/utils/farcaster-utils.js'; +import { supportsFarcasterDCs } from 'lib/utils/services-utils.js'; import FarcasterPrompt from './farcaster-prompt.react.js'; import FarcasterWebView, { @@ -17,6 +21,7 @@ import BottomSheet from '../bottom-sheet/bottom-sheet.react.js'; import type { RootNavigationProp } from '../navigation/root-navigator.react.js'; import type { NavigationRoute } from '../navigation/route-names.js'; +import ConnectFarcasterDCs from '../profile/connect-farcaster-dcs.react.js'; import { useTryLinkFID } from '../utils/farcaster-utils.js'; const farcasterPromptHeight = 350; @@ -37,8 +42,10 @@ React.useState('closed'); const [isLoadingLinkFID, setIsLoadingLinkFID] = React.useState(false); + const [showConnectDCs, setShowConnectDCs] = React.useState(false); const fid = useCurrentUserFID(); + const currentUserSupportsDCs = useCurrentUserSupportsDCs(); const tryLinkFID = useTryLinkFID(); @@ -48,6 +55,9 @@ try { await tryLinkFID(newFID); + if (supportsFarcasterDCs) { + setShowConnectDCs(true); + } } finally { setIsLoadingLinkFID(false); } @@ -71,19 +81,45 @@ React.useEffect(() => { if (fid && isAppForegrounded) { - goBack(); + if (currentUserSupportsDCs || !supportsFarcasterDCs) { + goBack(); + } else if (supportsFarcasterDCs && !showConnectDCs) { + setShowConnectDCs(true); + } } - }, [fid, goBack, isAppForegrounded]); + }, [fid, goBack, isAppForegrounded, currentUserSupportsDCs, showConnectDCs]); const onPressConnect = React.useCallback(() => { setIsLoadingLinkFID(true); setWebViewState('opening'); }, []); + const onConnectDCsSuccess = React.useCallback(() => { + setShowConnectDCs(false); + goBack(); + }, [goBack]); + + const onConnectDCsCancel = React.useCallback(() => { + setShowConnectDCs(false); + goBack(); + }, [goBack]); + const connectButtonVariant = isLoadingLinkFID ? 'loading' : 'enabled'; - const connectFarcasterBottomSheet = React.useMemo( - () => ( + const connectFarcasterBottomSheet = React.useMemo(() => { + if (showConnectDCs) { + return ( + + + + ); + } + + return ( @@ -97,9 +133,17 @@ - ), - [goBack, onPressConnect, connectButtonVariant, onSuccess, webViewState], - ); + ); + }, [ + showConnectDCs, + goBack, + onConnectDCsSuccess, + onConnectDCsCancel, + onPressConnect, + connectButtonVariant, + onSuccess, + webViewState, + ]); return connectFarcasterBottomSheet; } diff --git a/native/profile/connect-farcaster-dcs.react.js b/native/profile/connect-farcaster-dcs.react.js --- a/native/profile/connect-farcaster-dcs.react.js +++ b/native/profile/connect-farcaster-dcs.react.js @@ -1,5 +1,6 @@ // @flow +import { BottomSheetTextInput } from '@gorhom/bottom-sheet'; import * as React from 'react'; import { ScrollView, View } from 'react-native'; @@ -13,19 +14,25 @@ import PrimaryButton from '../components/primary-button.react.js'; import { FarcasterAuthContextProvider } from '../farcaster-auth/farcaster-auth-context-provider.react.js'; import { useGetAuthToken } from '../farcaster-auth/farcaster-auth-utils.js'; -import { useStyles } from '../themes/colors.js'; +import { + useStyles, + useColors, + useKeyboardAppearance, +} from '../themes/colors.js'; import Alert from '../utils/alert.js'; type Props = { +onSuccess: () => void, +onCancel: () => void, + +useBottomSheetTextInput?: boolean, }; function InnerConnectFarcasterDCs(props: Props): React.Node { - const { onSuccess, onCancel } = props; + const { onSuccess, onCancel, useBottomSheetTextInput = false } = props; const [mnemonic, setMnemonic] = React.useState(null); const [signingInProgress, setSigningInProgress] = React.useState(false); + const [focused, setFocused] = React.useState(false); const scrollViewRef = React.useRef>(null); @@ -58,6 +65,7 @@ }, []); const onInputFocus = React.useCallback(() => { + setFocused(true); // Scroll to make the input fully visible when focused setTimeout(() => { if (scrollViewRef.current) { @@ -66,6 +74,10 @@ }, 100); }, []); + const onInputBlur = React.useCallback(() => { + setFocused(false); + }, []); + let buttonVariant = 'enabled'; if (!mnemonic) { buttonVariant = 'disabled'; @@ -74,9 +86,71 @@ } const styles = useStyles(unboundStyles); + const colors = useColors(); + const keyboardAppearance = useKeyboardAppearance(); + + const textInputStyle = React.useMemo( + () => + focused + ? [styles.textInput, styles.focusedTextInput] + : [styles.textInput], + [focused, styles.textInput, styles.focusedTextInput], + ); + + const textInput = React.useMemo(() => { + if (useBottomSheetTextInput) { + return ( + + ); + } + return ( + + ); + }, [ + colors.panelSecondaryForegroundBorder, + keyboardAppearance, + mnemonic, + onChangeMnemonicText, + onConnect, + onInputBlur, + onInputFocus, + signingInProgress, + textInputStyle, + useBottomSheetTextInput, + ]); return ( - + <> - - - + {textInput} @@ -111,16 +170,11 @@ /> - + ); } const unboundStyles = { - container: { - flex: 1, - backgroundColor: 'panelBackground', - paddingBottom: 16, - }, scrollView: { flex: 1, }, @@ -138,6 +192,16 @@ marginVertical: 8, marginHorizontal: 16, }, + textInput: { + color: 'panelForegroundLabel', + borderColor: 'panelSecondaryForegroundBorder', + borderWidth: 1, + borderRadius: 4, + padding: 12, + }, + focusedTextInput: { + borderColor: 'panelForegroundLabel', + }, }; function ConnectFarcasterDCs(props: Props): React.Node { diff --git a/native/profile/farcaster-account-settings.react.js b/native/profile/farcaster-account-settings.react.js --- a/native/profile/farcaster-account-settings.react.js +++ b/native/profile/farcaster-account-settings.react.js @@ -174,10 +174,12 @@ return React.useMemo(() => { if (showConnectDCs) { return ( - + + + ); } @@ -203,6 +205,7 @@ showConnectDCs, styles.buttonContainer, styles.connectContainer, + styles.farcasterContainer, styles.promptContainer, styles.scrollViewContentContainer, webViewState, @@ -210,6 +213,11 @@ } const unboundStyles = { + farcasterContainer: { + flex: 1, + backgroundColor: 'panelBackground', + paddingBottom: 16, + }, connectContainer: { flex: 1, backgroundColor: 'panelBackground', diff --git a/native/redux/persist-constants.js b/native/redux/persist-constants.js --- a/native/redux/persist-constants.js +++ b/native/redux/persist-constants.js @@ -5,6 +5,6 @@ // NOTE: renaming this constant requires updating // `native/native_rust_library/build.rs` to correctly // scrap Redux state version from this file. -const storeVersion = 95; +const storeVersion = 96; export { rootKey, storeVersion }; diff --git a/native/redux/persist.js b/native/redux/persist.js --- a/native/redux/persist.js +++ b/native/redux/persist.js @@ -16,6 +16,7 @@ convertCalendarFilterToNewIDSchema, convertConnectionInfoToNewIDSchema, } from 'lib/_generated/migration-utils.js'; +import { sharedMigrations } from 'lib/backup/persist-shared-migrations.js'; import { extractKeyserverIDFromID } from 'lib/keyserver-conn/keyserver-call-utils.js'; import { convertQueuedDMOperationsStoreToAddOps } from 'lib/ops/dm-operations-store-ops.js'; import type { @@ -89,6 +90,7 @@ defaultAlertInfos, alertTypes, } from 'lib/types/alert-types.js'; +import { databaseIdentifier } from 'lib/types/database-identifier-types.js'; import { dmOperationTypes } from 'lib/types/dm-ops.js'; import { defaultEnabledApps } from 'lib/types/enabled-apps.js'; import { defaultCalendarQuery } from 'lib/types/entry-types.js'; @@ -1707,6 +1709,13 @@ }, }; }: MigrationFunction), + [96]: (async (state: AppState) => { + const ops = await sharedMigrations[96](databaseIdentifier.MAIN); + return { + state, + ops, + }; + }: MigrationFunction), }); const persistConfig = { diff --git a/web/redux/persist-constants.js b/web/redux/persist-constants.js --- a/web/redux/persist-constants.js +++ b/web/redux/persist-constants.js @@ -3,6 +3,6 @@ const rootKey = 'root'; const rootKeyPrefix = 'persist:'; const completeRootKey = `${rootKeyPrefix}${rootKey}`; -const storeVersion = 95; +const storeVersion = 96; export { rootKey, rootKeyPrefix, completeRootKey, storeVersion }; diff --git a/web/redux/persist.js b/web/redux/persist.js --- a/web/redux/persist.js +++ b/web/redux/persist.js @@ -6,6 +6,7 @@ import storage from 'redux-persist/es/storage/index.js'; import type { PersistConfig } from 'redux-persist/src/types.js'; +import { sharedMigrations } from 'lib/backup/persist-shared-migrations.js'; import { createReplaceThreadOperation } from 'lib/ops/create-replace-thread-operation.js'; import { convertQueuedDMOperationsStoreToAddOps } from 'lib/ops/dm-operations-store-ops.js'; import { @@ -37,6 +38,7 @@ import { messageStoreMessagesBlocklistTransform } from 'lib/shared/transforms/message-store-transform.js'; import { unshimDMOperations } from 'lib/shared/unshim-utils.js'; import { defaultAlertInfos } from 'lib/types/alert-types.js'; +import { databaseIdentifier } from 'lib/types/database-identifier-types.js'; import { dmOperationTypes } from 'lib/types/dm-ops.js'; import { defaultCalendarQuery } from 'lib/types/entry-types.js'; import type { KeyserverInfo } from 'lib/types/keyserver-types.js'; @@ -916,6 +918,13 @@ }, }; }: MigrationFunction), + [96]: (async (state: AppState) => { + const ops = await sharedMigrations[96](databaseIdentifier.MAIN); + return { + state, + ops, + }; + }: MigrationFunction), }; const persistConfig: PersistConfig = {