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 @@ -15,6 +15,7 @@ useCurrentUserFID, useUnlinkFID, useSetLocalFID, + useSetLocalCurrentUserSupportsDCs, } from '../utils/farcaster-utils.js'; import { useDispatchActionPromise } from '../utils/redux-promise-utils.js'; import { useSelector, useDispatch } from '../utils/redux-utils.js'; @@ -157,6 +158,8 @@ const [fidLoaded, setFIDLoaded] = React.useState(false); const setLocalFID = useSetLocalFID(); + const setLocalDCsSupport = useSetLocalCurrentUserSupportsDCs(); + const handleCurrentUserFID = React.useCallback(async () => { if ( canQueryHandleCurrentUserFID === @@ -178,6 +181,10 @@ ? false : getCachedUserIdentity(currentUserID) === undefined; + if (userIdentities[currentUserID]) { + setLocalDCsSupport(userIdentities[currentUserID].hasFarcasterDCsToken); + } + if (fid && !identityFIDRequestTimedOut && fid !== identityFID) { setLocalFID(identityFID); return; @@ -204,6 +211,7 @@ unlinkFID, setLocalFID, getCachedUserIdentity, + setLocalDCsSupport, ]); React.useEffect(() => { diff --git a/lib/tunnelbroker/use-peer-to-peer-message-handler.js b/lib/tunnelbroker/use-peer-to-peer-message-handler.js --- a/lib/tunnelbroker/use-peer-to-peer-message-handler.js +++ b/lib/tunnelbroker/use-peer-to-peer-message-handler.js @@ -51,6 +51,10 @@ import { getConfig } from '../utils/config.js'; import { getContentSigningKey } from '../utils/crypto-utils.js'; import { getMessageForException } from '../utils/errors.js'; +import { + useSetLocalCurrentUserSupportsDCs, + useSetLocalFID, +} from '../utils/farcaster-utils.js'; import { hasHigherDeviceID, OLM_ERROR_FLAG, @@ -99,6 +103,9 @@ const { userDataRestore } = useUserDataRestoreContext(); const restoreBackupState = useSelector(state => state.restoreBackupState); + const setLocalFID = useSetLocalFID(); + const setLocalDCsSupport = useSetLocalCurrentUserSupportsDCs(); + return React.useCallback( async ( decryptedMessageContent: string, @@ -208,6 +215,12 @@ await userDataRestore(false, userID, accessToken, backupData); await sqliteAPI.removeInboundP2PMessages([messageID]); + } else if ( + userActionMessage.type === + userActionsP2PMessageTypes.FARCASTER_CONNECTION_UPDATED + ) { + setLocalFID(userActionMessage.farcasterID); + setLocalDCsSupport(userActionMessage.hasDCsToken); } else { console.warn( 'Unsupported P2P user action message:', @@ -224,6 +237,8 @@ reBroadcastAccountDeletion, restoreBackupState.status, runDeviceListUpdate, + setLocalDCsSupport, + setLocalFID, userDataRestore, ], ); diff --git a/lib/types/tunnelbroker/peer-to-peer-message-types.js b/lib/types/tunnelbroker/peer-to-peer-message-types.js --- a/lib/types/tunnelbroker/peer-to-peer-message-types.js +++ b/lib/types/tunnelbroker/peer-to-peer-message-types.js @@ -116,7 +116,7 @@ }; export const badDeviceTokenValidator: TInterface = tShape({ - type: tString('BadDeviceToken'), + type: tString(peerToPeerMessageTypes.BAD_DEVICE_TOKEN), invalidatedToken: t.String, }); 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 @@ -13,6 +13,7 @@ ACCOUNT_DELETION: 'ACCOUNT_DELETION', DM_OPERATION: 'DM_OPERATION', BACKUP_DATA: 'BACKUP_DATA', + FARCASTER_CONNECTION_UPDATED: 'FARCASTER_CONNECTION_UPDATED', }); export type DeviceLogoutP2PMessage = { @@ -66,12 +67,25 @@ backupData: t.maybe(qrAuthBackupDataValidator), }); +export type FarcasterConnectionUpdated = { + +type: 'FARCASTER_CONNECTION_UPDATED', + +farcasterID: ?string, + +hasDCsToken: ?boolean, +}; +export const farcasterConnectionUpdatedValidator: TInterface = + tShape({ + type: tString(userActionsP2PMessageTypes.FARCASTER_CONNECTION_UPDATED), + farcasterID: t.maybe(t.String), + hasDCsToken: t.maybe(t.Boolean), + }); + export type UserActionP2PMessage = | DeviceLogoutP2PMessage | SecondaryDeviceLogoutP2PMessage | AccountDeletionP2PMessage | DMOperationP2PMessage - | BackupDataP2PMessage; + | BackupDataP2PMessage + | FarcasterConnectionUpdated; export const userActionP2PMessageValidator: TUnion = t.union([ @@ -80,4 +94,5 @@ accountDeletionP2PMessageValidator, dmOperationP2PMessageValidator, backupDataP2PMessageValidator, + farcasterConnectionUpdatedValidator, ]); 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 @@ -2,12 +2,20 @@ import invariant from 'invariant'; import * as React from 'react'; +import uuid from 'uuid'; +import { getConfig } from './config.js'; +import { getContentSigningKey } from './crypto-utils.js'; import { useSelector, useDispatch } from './redux-utils.js'; import { setSyncedMetadataEntryActionType } from '../actions/synced-metadata-actions.js'; import { useUserIdentityCache } from '../components/user-identity-cache.react.js'; +import { getOwnPeerDevices } from '../selectors/user-selectors.js'; import { IdentityClientContext } from '../shared/identity-client-context.js'; +import { PeerToPeerContext } from '../tunnelbroker/peer-to-peer-context.js'; +import { databaseIdentifier } from '../types/database-identifier-types.js'; +import { outboundP2PMessageStatuses } from '../types/sqlite-types.js'; import { syncedMetadataNames } from '../types/synced-metadata-types.js'; +import type { FarcasterConnectionUpdated } from '../types/tunnelbroker/user-actions-peer-to-peer-message-types.js'; const DISABLE_CONNECT_FARCASTER_ALERT = false; const NO_FID_METADATA = 'NONE'; @@ -108,13 +116,16 @@ const { linkFarcasterAccount } = identityClient; const setLocalFID = useSetLocalFID(); + const broadcastConnectionStatus = + useBroadcastUpdateFarcasterConnectionStatus(); return React.useCallback( async (fid: string) => { await linkFarcasterAccount(fid); setLocalFID(fid); + await broadcastConnectionStatus(fid, null); }, - [setLocalFID, linkFarcasterAccount], + [linkFarcasterAccount, setLocalFID, broadcastConnectionStatus], ); } @@ -127,12 +138,20 @@ const setLocalFID = useSetLocalFID(); const setLocalDCsSupport = useSetLocalCurrentUserSupportsDCs(); + const broadcastConnectionStatus = + useBroadcastUpdateFarcasterConnectionStatus(); return React.useCallback(async () => { await unlinkFarcasterAccount(); setLocalFID(null); setLocalDCsSupport(null); - }, [setLocalFID, setLocalDCsSupport, unlinkFarcasterAccount]); + await broadcastConnectionStatus(null, null); + }, [ + unlinkFarcasterAccount, + setLocalFID, + setLocalDCsSupport, + broadcastConnectionStatus, + ]); } function useLinkFarcasterDCs(): ( @@ -146,13 +165,60 @@ const { linkFarcasterDCsAccount } = identityClient; const setLocalDCsSupport = useSetLocalCurrentUserSupportsDCs(); + const broadcastConnectionStatus = + useBroadcastUpdateFarcasterConnectionStatus(); return React.useCallback( async (fid: string, farcasterDCsToken: string) => { await linkFarcasterDCsAccount(fid, farcasterDCsToken); setLocalDCsSupport(true); + await broadcastConnectionStatus(fid, true); }, - [setLocalDCsSupport, linkFarcasterDCsAccount], + [linkFarcasterDCsAccount, setLocalDCsSupport, broadcastConnectionStatus], + ); +} + +function useBroadcastUpdateFarcasterConnectionStatus() { + const peerToPeerContext = React.useContext(PeerToPeerContext); + const { processDBStoreOperations } = getConfig().sqliteAPI; + + const currentUserID = useSelector(state => state.currentUserInfo?.id); + const userDevices = useSelector(getOwnPeerDevices); + return React.useCallback( + async (farcasterID: ?string, hasDCsToken: ?boolean) => { + if (!currentUserID) { + return; + } + invariant(peerToPeerContext, 'PeerToPeerContext should be set'); + const thisDeviceID = await getContentSigningKey(); + const message: FarcasterConnectionUpdated = { + type: 'FARCASTER_CONNECTION_UPDATED', + farcasterID, + hasDCsToken, + }; + const messageString = JSON.stringify(message); + const timestamp = new Date().getTime().toString(); + const messages = userDevices + .filter(device => device.deviceID !== thisDeviceID) + .map(device => ({ + messageID: uuid.v4(), + deviceID: device.deviceID, + userID: currentUserID, + timestamp, + plaintext: messageString, + ciphertext: '', + status: outboundP2PMessageStatuses.persisted, + supportsAutoRetry: true, + })); + await processDBStoreOperations( + { outboundP2PMessages: messages }, + databaseIdentifier.MAIN, + ); + await peerToPeerContext.processOutboundMessages( + messages.map(m => m.messageID), + ); + }, + [currentUserID, peerToPeerContext, processDBStoreOperations, userDevices], ); }