From a5f9a3929c495db94a2495ed4151e62051133528 Mon Sep 17 00:00:00 2001 From: Chris Nojima Date: Fri, 27 Mar 2026 15:26:39 -0400 Subject: [PATCH 01/14] proof slim --- shared/profile/confirm-or-pending.tsx | 29 +- shared/profile/edit-avatar/hooks.tsx | 6 +- shared/profile/edit-profile.tsx | 17 +- shared/profile/generic/enter-username.tsx | 81 +- shared/profile/generic/result.tsx | 33 +- shared/profile/pgp/finished/index.desktop.tsx | 14 +- shared/profile/pgp/info/index.desktop.tsx | 50 +- shared/profile/pgp/validation.ts | 22 + shared/profile/post-proof.tsx | 26 +- shared/profile/proof-utils.ts | 30 + shared/profile/prove-enter-username.tsx | 87 ++- shared/profile/revoke.tsx | 44 +- shared/stores/profile.tsx | 712 ++++++------------ shared/stores/tests/profile.test.ts | 103 +-- shared/tracker/assertion.tsx | 22 +- .../references/store-checklist.md | 4 +- 16 files changed, 574 insertions(+), 706 deletions(-) create mode 100644 shared/profile/pgp/validation.ts create mode 100644 shared/profile/proof-utils.ts diff --git a/shared/profile/confirm-or-pending.tsx b/shared/profile/confirm-or-pending.tsx index f33f9325224e..9dc09b964177 100644 --- a/shared/profile/confirm-or-pending.tsx +++ b/shared/profile/confirm-or-pending.tsx @@ -3,13 +3,25 @@ import * as T from '@/constants/types' import * as Kb from '@/common-adapters' import {subtitle} from '@/util/platforms' import Modal from './modal' +import {useCurrentUserState} from '@/stores/current-user' +import * as C from '@/constants' -const ConfirmOrPending = () => { - const proofFound = useProfileState(s => s.proofFound) - const proofStatus = useProfileState(s => s.proofStatus) - const platform = useProfileState(s => s.platform) - const username = useProfileState(s => s.username) - const backToProfile = useProfileState(s => s.dispatch.backToProfile) +type Props = { + route: { + params: { + platform: T.More.PlatformsExpandedType + proofFound: boolean + proofStatus?: T.RPCGen.ProofStatus + username: string + } + } +} + +const ConfirmOrPending = ({route}: Props) => { + const {platform, proofFound, proofStatus, username} = route.params + const showUserProfile = useProfileState(s => s.dispatch.showUserProfile) + const currentUsername = useCurrentUserState(s => s.username) + const clearModals = C.useRouterState(s => s.dispatch.clearModals) const isGood = proofFound && proofStatus === T.RPCGen.ProofStatus.ok const isPending = @@ -20,7 +32,10 @@ const ConfirmOrPending = () => { } const platformIconOverlayColor = isGood ? Kb.Styles.globalColors.green : Kb.Styles.globalColors.greyDark - const onCancel = backToProfile + const onCancel = () => { + clearModals() + showUserProfile(currentUsername) + } const message = messageMap.get(platform) || diff --git a/shared/profile/edit-avatar/hooks.tsx b/shared/profile/edit-avatar/hooks.tsx index 9e5778f69af5..b817e4a88c9c 100644 --- a/shared/profile/edit-avatar/hooks.tsx +++ b/shared/profile/edit-avatar/hooks.tsx @@ -1,11 +1,11 @@ import * as React from 'react' import * as C from '@/constants' -import {useProfileState} from '@/stores/profile' import * as Teams from '@/stores/teams' import * as Kb from '@/common-adapters' import * as T from '@/constants/types' import type {Props} from '.' import type {ImageInfo} from '@/util/expo-image-picker.native' +import {fixCrop} from '@/util/crop' type TeamProps = { createdTeam?: boolean @@ -77,11 +77,11 @@ export default (ownProps: Props): Ret => { uploadTeamAvatar(teamname, filename, sendChatNotification, crop) } - const uploadAvatar = useProfileState(s => s.dispatch.uploadAvatar) + const uploadAvatar = C.useRPC(T.RPCGen.userUploadUserAvatarRpcPromise) const onSaveUserAvatar = (_filename: string, crop?: T.RPCGen.ImageCropRect) => { const filename = Kb.Styles.unnormalizePath(_filename) - uploadAvatar(filename, crop) + uploadAvatar([{crop: fixCrop(crop), filename}, C.waitingKeyProfileUploadAvatar], () => navigateUp(), () => {}) } const setTeamWizardAvatar = Teams.useTeamsState(s => s.dispatch.setTeamWizardAvatar) const onSaveWizardAvatar = (_filename: string, crop?: T.Teams.AvatarCrop) => { diff --git a/shared/profile/edit-profile.tsx b/shared/profile/edit-profile.tsx index f0decc39167a..fc767ff823cc 100644 --- a/shared/profile/edit-profile.tsx +++ b/shared/profile/edit-profile.tsx @@ -2,22 +2,29 @@ import * as C from '@/constants' import * as Kb from '@/common-adapters' import * as React from 'react' import {useTrackerState} from '@/stores/tracker' -import {useProfileState} from '@/stores/profile' import {useCurrentUserState} from '@/stores/current-user' +import {generateGUIID} from '@/constants/utils' +import * as T from '@/constants/types' const Container = () => { const username = useCurrentUserState(s => s.username) const d = useTrackerState(s => s.getDetails(username)) + const loadProfile = useTrackerState(s => s.dispatch.load) const _bio = d.bio || '' const _fullname = d.fullname || '' const _location = d.location || '' const navigateUp = C.useRouterState(s => s.dispatch.navigateUp) - - const editProfile = useProfileState(s => s.dispatch.editProfile) + const editProfile = C.useRPC(T.RPCGen.userProfileEditRpcPromise) const onSubmit = (bio: string, fullname: string, location: string) => { - editProfile(bio, fullname, location) - navigateUp() + editProfile( + [{bio, fullName: fullname, location}, C.waitingKeyTracker], + () => { + loadProfile({assertion: username, guiID: generateGUIID(), inTracker: false, reason: ''}) + navigateUp() + }, + () => {} + ) } const [bio, setBio] = React.useState(_bio) diff --git a/shared/profile/generic/enter-username.tsx b/shared/profile/generic/enter-username.tsx index a5e585332046..f7152f7ccaf3 100644 --- a/shared/profile/generic/enter-username.tsx +++ b/shared/profile/generic/enter-username.tsx @@ -1,32 +1,34 @@ import * as C from '@/constants' -import {useProfileState} from '@/stores/profile' +import {type ProveGenericParams, useProfileState} from '@/stores/profile' import {openURL} from '@/util/misc' import * as React from 'react' import * as Kb from '@/common-adapters' import {SiteIcon} from './shared' import type * as T from '@/constants/types' -const ConnectedEnterUsername = () => { - const {platformGenericChecking, platformGenericParams, platformGenericURL, username} = useProfileState( - C.useShallow(s => { - const {platformGenericChecking, platformGenericParams, platformGenericURL, username} = s - return {platformGenericChecking, platformGenericParams, platformGenericURL, username} - }) - ) - const errorText = useProfileState(s => s.errorText) - const _platformURL = platformGenericURL - const error = errorText - const serviceIcon = platformGenericParams?.logoBlack ?? [] - const serviceIconFull = platformGenericParams?.logoFull ?? [] - const serviceName = platformGenericParams?.title ?? '' - const serviceSub = platformGenericParams?.subtext ?? '' - const serviceSuffix = platformGenericParams?.suffix ?? '' - const submitButtonLabel = platformGenericParams?.buttonLabel ?? 'Submit' - const unreachable = !!platformGenericURL - const waiting = platformGenericChecking +type Props = { + route: { + params: { + error?: string + genericParams: ProveGenericParams + proofUrl?: string + service: string + username?: string + } + } +} + +const ConnectedEnterUsername = ({route}: Props) => { + const {error = '', genericParams, proofUrl, username: routeUsername = ''} = route.params + const serviceIcon = genericParams.logoBlack ?? [] + const serviceIconFull = genericParams.logoFull ?? [] + const serviceName = genericParams.title ?? '' + const serviceSub = genericParams.subtext ?? '' + const serviceSuffix = genericParams.suffix ?? '' + const submitButtonLabel = genericParams.buttonLabel ?? 'Submit' + const unreachable = !!proofUrl const cancelAddProof = useProfileState(s => s.dispatch.dynamic.cancelAddProof) - const updateUsername = useProfileState(s => s.dispatch.updateUsername) const submitUsername = useProfileState(s => s.dispatch.dynamic.submitUsername) const clearModals = C.useRouterState(s => s.dispatch.clearModals) @@ -34,25 +36,12 @@ const ConnectedEnterUsername = () => { cancelAddProof?.() clearModals() } - const onChangeUsername = updateUsername - const navigateAppend = C.useRouterState(s => s.dispatch.navigateAppend) - const _onSubmit = () => submitUsername?.() - const onSubmit = _platformURL ? () => _platformURL && openURL(_platformURL) : _onSubmit - - const [waitingButtonKey, setWaitingButtonKey] = React.useState(0) - const wasWaiting = React.useRef(false) + const [username, setUsername] = React.useState(routeUsername) + const onSubmit = proofUrl ? () => openURL(proofUrl) : () => submitUsername?.(username) React.useEffect(() => { - if (waiting) { - wasWaiting.current = true - } else if (wasWaiting.current) { - wasWaiting.current = false - navigateAppend('profileGenericProofResult') - } - if (error) { - setWaitingButtonKey(s => s + 1) - } - }, [waiting, error, navigateAppend]) + setUsername(routeUsername) + }, [routeUsername]) return ( <> @@ -95,7 +84,7 @@ const ConnectedEnterUsername = () => { serviceIcon={serviceIcon} serviceSuffix={serviceSuffix} username={username} - onChangeUsername={onChangeUsername} + onChangeUsername={setUsername} onEnterKeyDown={onSubmit} /> )} @@ -129,7 +118,7 @@ const ConnectedEnterUsername = () => { onClick={onSubmit} label={submitButtonLabel} style={styles.buttonBig} - key={waitingButtonKey} + waitingKey={C.waitingKeyProfile} /> )} @@ -149,26 +138,18 @@ type InputProps = { } const EnterUsernameInput = (props: InputProps) => { - const [username, setUsername] = React.useState(props.username) - const {onChangeUsername: _onChangeUsername} = props - - const onChangeUsername = (username: string) => { - _onChangeUsername(username) - setUsername(username) - } - const usernamePlaceholder = props.serviceSuffix === '@theqrl.org' ? 'Your QRL address' : 'Your username' return ( { - const errorText = useProfileState(s => - s.errorCode !== undefined ? s.errorText || 'Failed to verify proof' : '' - ) - const proofUsername = useProfileState(s => s.username + (s.platformGenericParams?.suffix ?? '@unknown')) - const serviceIcon = useProfileState(s => s.platformGenericParams?.logoFull ?? []) - const backToProfile = useProfileState(s => s.dispatch.backToProfile) - const clearPlatformGeneric = useProfileState(s => s.dispatch.clearPlatformGeneric) +type Props = { + route: { + params: { + error?: string + genericParams: ProveGenericParams + username: string + } + } +} + +const GenericResult = ({route}: Props) => { + const {error = '', genericParams, username} = route.params + const proofUsername = username + (genericParams.suffix ?? '@unknown') + const serviceIcon = genericParams.logoFull ?? [] + const showUserProfile = useProfileState(s => s.dispatch.showUserProfile) + const currentUsername = useCurrentUserState(s => s.username) const clearModals = C.useRouterState(s => s.dispatch.clearModals) const onClose = () => { clearModals() - backToProfile() - clearPlatformGeneric() + showUserProfile(currentUsername) } - const success = !errorText + const success = !error const iconType = success ? 'icon-proof-success' : 'icon-proof-broken' let frag = ( <> @@ -29,7 +38,7 @@ const GenericResult = () => { if (!success) { frag = ( <> - {errorText} + {error} ) } diff --git a/shared/profile/pgp/finished/index.desktop.tsx b/shared/profile/pgp/finished/index.desktop.tsx index 22e9341df3e8..d984ee7ea5fd 100644 --- a/shared/profile/pgp/finished/index.desktop.tsx +++ b/shared/profile/pgp/finished/index.desktop.tsx @@ -76,9 +76,17 @@ const styles = Kb.Styles.styleSheetCreate( }) as const ) -const Container = () => { - const pgpKeyString = useProfileState(s => s.pgpPublicKey || 'Error getting public key...') - const promptShouldStoreKeyOnServer = useProfileState(s => s.promptShouldStoreKeyOnServer) +type ContainerProps = { + route: { + params: { + pgpKeyString: string + promptShouldStoreKeyOnServer: boolean + } + } +} + +const Container = ({route}: ContainerProps) => { + const {pgpKeyString, promptShouldStoreKeyOnServer} = route.params const finishedWithKeyGen = useProfileState(s => s.dispatch.dynamic.finishedWithKeyGen) const clearModals = C.useRouterState(s => s.dispatch.clearModals) diff --git a/shared/profile/pgp/info/index.desktop.tsx b/shared/profile/pgp/info/index.desktop.tsx index 9de17aec59b7..24951f7f7574 100644 --- a/shared/profile/pgp/info/index.desktop.tsx +++ b/shared/profile/pgp/info/index.desktop.tsx @@ -1,35 +1,33 @@ import * as C from '@/constants' import {useProfileState} from '@/stores/profile' +import * as React from 'react' import * as Kb from '@/common-adapters' import Modal from '@/profile/modal' +import {validatePgpInfo} from '../validation' const Info = () => { - const updatePgpInfo = useProfileState(s => s.dispatch.updatePgpInfo) const generatePgp = useProfileState(s => s.dispatch.generatePgp) - const data = useProfileState( - C.useShallow(s => { - const {pgpEmail1, pgpEmail2, pgpEmail3, pgpErrorText, pgpFullName} = s - const {pgpErrorEmail1, pgpErrorEmail2, pgpErrorEmail3} = s - return { - pgpEmail1, - pgpEmail2, - pgpEmail3, - pgpErrorEmail1, - pgpErrorEmail2, - pgpErrorEmail3, - pgpErrorText, - pgpFullName, - } - }) - ) + const [pgpFullName, setPgpFullName] = React.useState('') + const [pgpEmail1, setPgpEmail1] = React.useState('') + const [pgpEmail2, setPgpEmail2] = React.useState('') + const [pgpEmail3, setPgpEmail3] = React.useState('') + const data = { + pgpEmail1, + pgpEmail2, + pgpEmail3, + pgpFullName, + ...validatePgpInfo({pgpEmail1, pgpEmail2, pgpEmail3, pgpFullName}), + } const navigateUp = C.useRouterState(s => s.dispatch.navigateUp) const onCancel = () => navigateUp() - const onChangeEmail1 = (pgpEmail1: string) => updatePgpInfo({pgpEmail1}) - const onChangeEmail2 = (pgpEmail2: string) => updatePgpInfo({pgpEmail2}) - const onChangeEmail3 = (pgpEmail3: string) => updatePgpInfo({pgpEmail3}) - const onChangeFullName = (pgpFullName: string) => updatePgpInfo({pgpFullName}) - const onNext = () => generatePgp() + const onNext = () => + generatePgp({ + pgpEmail1: data.pgpEmail1, + pgpEmail2: data.pgpEmail2, + pgpEmail3: data.pgpEmail3, + pgpFullName: data.pgpFullName, + }) const nextDisabled = !data.pgpEmail1 || !data.pgpFullName || !!data.pgpErrorText return ( @@ -42,25 +40,25 @@ const Info = () => { autoFocus={true} placeholder="Your full name" value={data.pgpFullName} - onChangeText={onChangeFullName} + onChangeText={setPgpFullName} /> { + const email1Error = Validators.isValidEmail(info.pgpEmail1) + const email2Error = info.pgpEmail2 ? Validators.isValidEmail(info.pgpEmail2) : '' + const email3Error = info.pgpEmail3 ? Validators.isValidEmail(info.pgpEmail3) : '' + const nameError = Validators.isValidName(info.pgpFullName) + + return { + pgpErrorEmail1: !!email1Error, + pgpErrorEmail2: !!email2Error, + pgpErrorEmail3: !!email3Error, + pgpErrorText: nameError || email1Error || email2Error || email3Error, + } +} diff --git a/shared/profile/post-proof.tsx b/shared/profile/post-proof.tsx index 222dd075bcd8..df270d3fd367 100644 --- a/shared/profile/post-proof.tsx +++ b/shared/profile/post-proof.tsx @@ -6,14 +6,24 @@ import {subtitle} from '@/util/platforms' import {openURL as openUrl} from '@/util/misc' import Modal from './modal' import {useConfigState} from '@/stores/config' +import type * as T from '@/constants/types' -const Container = () => { - const platform = useProfileState(s => s.platform) - const errorText = useProfileState(s => s.errorText) - const username = useProfileState(s => s.username) - let proofText = useProfileState(s => s.proofText) +type Props = { + route: { + params: { + error?: string + platform: T.More.PlatformsExpandedType + proofText: string + username: string + } + } +} + +const Container = ({route}: Props) => { + const {error, platform, username} = route.params + let {proofText} = route.params const cancelAddProof = useProfileState(s => s.dispatch.dynamic.cancelAddProof) - const checkProof = useProfileState(s => s.dispatch.checkProof) + const afterCheckProof = useProfileState(s => s.dispatch.dynamic.afterCheckProof) if ( !platform || platform === 'zcash' || @@ -55,8 +65,8 @@ const Container = () => { clearModals() cancelAddProof?.() } - const onSubmit = checkProof - const errorMessage = errorText + const onSubmit = () => afterCheckProof?.() + const errorMessage = error ?? '' const [showSubmit, setShowSubmit] = React.useState(!openLinkBeforeSubmit) const platformSubtitle = subtitle(platform) const proofActionText = actionMap.get(platform) ?? '' diff --git a/shared/profile/proof-utils.ts b/shared/profile/proof-utils.ts new file mode 100644 index 000000000000..dc522449e014 --- /dev/null +++ b/shared/profile/proof-utils.ts @@ -0,0 +1,30 @@ +import * as T from '@/constants/types' + +const isValidBitcoinAddress = (username: string) => { + const legacyFormat = username.search(/^[13][a-km-zA-HJ-NP-Z1-9]{25,34}$/) !== -1 + const segwitFormat = + username.toLowerCase().search(/^(bc1)[qpzry9x8gf2tvdw0s3jn54khce6mua7l]{11,71}$/) !== -1 + return legacyFormat || segwitFormat +} + +export const normalizeProofUsername = ( + platform: T.More.PlatformsExpandedType | undefined, + username: string +) => { + let normalized = username + let valid = true + + switch (platform) { + case 'http': + case 'https': + normalized = normalized.replace(/^.*?:\/\//, '').replace(/:.*/, '').replace(/\/.*/, '') + break + case 'btc': + valid = isValidBitcoinAddress(normalized) + break + default: + break + } + + return {normalized, valid} +} diff --git a/shared/profile/prove-enter-username.tsx b/shared/profile/prove-enter-username.tsx index 31fcaa26a870..b00436d8cb37 100644 --- a/shared/profile/prove-enter-username.tsx +++ b/shared/profile/prove-enter-username.tsx @@ -4,31 +4,85 @@ import * as React from 'react' import * as Kb from '@/common-adapters' import Modal from './modal' import type * as T from '@/constants/types' +import {normalizeProofUsername} from './proof-utils' + +type Props = { + route: { + params: { + error?: string + platform: T.More.PlatformsExpandedType + username?: string + } + } +} -const Container = () => { - const platform = useProfileState(s => s.platform) - const _errorText = useProfileState(s => s.errorText) - const updateUsername = useProfileState(s => s.dispatch.updateUsername) +const Container = ({route}: Props) => { + const {error: routeError, platform, username: initialUsername = ''} = route.params const cancelAddProof = useProfileState(s => s.dispatch.dynamic.cancelAddProof) - const submitBTCAddress = useProfileState(s => s.dispatch.submitBTCAddress) - const submitZcashAddress = useProfileState(s => s.dispatch.submitZcashAddress) const submitUsername = useProfileState(s => s.dispatch.dynamic.submitUsername) + const registerCryptoAddress = C.useRPC(T.RPCGen.cryptocurrencyRegisterAddressRpcPromise) + const [username, setUsername] = React.useState(initialUsername) + const [errorText, setErrorText] = React.useState(routeError === 'Input canceled' ? '' : (routeError ?? '')) + const [canSubmit, setCanSubmit] = React.useState(!!initialUsername.length) - if (!platform) { - throw new Error('No platform passed to prove enter username') - } + React.useEffect(() => { + setErrorText(routeError === 'Input canceled' ? '' : (routeError ?? '')) + }, [routeError]) - const errorText = _errorText === 'Input canceled' ? '' : _errorText + React.useEffect(() => { + setUsername(initialUsername) + setCanSubmit(!!initialUsername.length) + }, [initialUsername]) - const _onSubmit = (username: string, platform?: string) => { - updateUsername(username) + const navigateAppend = C.useRouterState(s => s.dispatch.navigateAppend) + const _onSubmit = (input: string, platform?: string) => { + const {normalized, valid} = normalizeProofUsername(platform as T.More.PlatformsExpandedType | undefined, input) if (platform === 'btc') { - submitBTCAddress() + if (!valid) { + setErrorText('Invalid address format') + return + } + setErrorText('') + registerCryptoAddress( + [{address: normalized, force: true, wantedFamily: 'bitcoin'}, C.waitingKeyProfile], + () => { + navigateAppend({ + name: 'profileConfirmOrPending', + params: { + platform, + proofFound: true, + proofStatus: T.RPCGen.ProofStatus.ok, + username: normalized, + }, + }) + }, + error => { + setErrorText(error.desc) + } + ) } else if (platform === 'zcash') { - submitZcashAddress() + setErrorText('') + registerCryptoAddress( + [{address: normalized, force: true, wantedFamily: 'zcash'}, C.waitingKeyProfile], + () => { + navigateAppend({ + name: 'profileConfirmOrPending', + params: { + platform, + proofFound: true, + proofStatus: T.RPCGen.ProofStatus.ok, + username: normalized, + }, + }) + }, + error => { + setErrorText(error.desc) + } + ) } else { - submitUsername?.() + setErrorText('') + submitUsername?.(normalized) } } const clearModals = C.useRouterState(s => s.dispatch.clearModals) @@ -38,9 +92,6 @@ const Container = () => { } const onSubmit = (username: string) => _onSubmit(username, platform) - const [username, setUsername] = React.useState('') - const [canSubmit, setCanSubmit] = React.useState(false) - const submit = () => { if (canSubmit) { onSubmit(username) diff --git a/shared/profile/revoke.tsx b/shared/profile/revoke.tsx index d20ec5b14254..f57032754798 100644 --- a/shared/profile/revoke.tsx +++ b/shared/profile/revoke.tsx @@ -1,11 +1,15 @@ import * as C from '@/constants' import {useProfileState} from '@/stores/profile' import * as Kb from '@/common-adapters' +import * as React from 'react' import capitalize from 'lodash/capitalize' import {subtitle as platformSubtitle} from '@/util/platforms' import {SiteIcon} from './generic/shared' import type * as T from '@/constants/types' import Modal from './modal' +import {useCurrentUserState} from '@/stores/current-user' +import {useTrackerState} from '@/stores/tracker' +import {generateGUIID} from '@/constants/utils' type OwnProps = { icon: T.Tracker.SiteIconSet @@ -15,17 +19,45 @@ type OwnProps = { } const RevokeProof = (ownProps: OwnProps) => { const {platformHandle, platform, proofId, icon} = ownProps - const errorMessage = useProfileState(s => s.revokeError) - const finishRevoking = useProfileState(s => s.dispatch.finishRevoking) - const submitRevokeProof = useProfileState(s => s.dispatch.submitRevokeProof) + const [errorMessage, setErrorMessage] = React.useState('') + const currentUsername = useCurrentUserState(s => s.username) + const assertions = useTrackerState(s => s.getDetails(currentUsername).assertions) + const loadProfile = useTrackerState(s => s.dispatch.load) + const showUserProfile = useProfileState(s => s.dispatch.showUserProfile) + const revokeKey = C.useRPC(T.RPCGen.revokeRevokeKeyRpcPromise) + const revokeSigs = C.useRPC(T.RPCGen.revokeRevokeSigsRpcPromise) const clearModals = C.useRouterState(s => s.dispatch.clearModals) + const proof = assertions ? [...assertions.values()].find(a => a.sigID === proofId) : undefined + const onSuccess = () => { + showUserProfile(currentUsername) + loadProfile({assertion: currentUsername, guiID: generateGUIID(), inTracker: false, reason: ''}) + clearModals() + } const onCancel = () => { - finishRevoking() clearModals() } const onRevoke = () => { - proofId && submitRevokeProof(proofId) - clearModals() + if (!proofId || !proof) { + clearModals() + return + } + if (proof.type === 'pgp') { + revokeKey( + [{keyID: proof.kid}, C.waitingKeyProfile], + onSuccess, + error => { + setErrorMessage(`Error in dropping Pgp Key: ${error.message}`) + } + ) + return + } + revokeSigs( + [{sigIDQueries: [proofId]}, C.waitingKeyProfile], + onSuccess, + () => { + setErrorMessage('There was an error revoking your proof. You can click the button to try again.') + } + ) } const platformHandleSubtitle = platformSubtitle(platform) diff --git a/shared/stores/profile.tsx b/shared/stores/profile.tsx index 8ecd5124b00b..9249aa146d15 100644 --- a/shared/stores/profile.tsx +++ b/shared/stores/profile.tsx @@ -1,17 +1,16 @@ import * as T from '@/constants/types' import {generateGUIID, ignorePromise, wrapErrors} from '@/constants/utils' import * as S from '@/constants/strings' -import * as Validators from '@/util/simple-validators' import * as Z from '@/util/zustand' import logger from '@/logger' import {openURL} from '@/util/misc' import {RPCError} from '@/util/errors' -import {fixCrop} from '@/util/crop' -import {clearModals, navToProfile, navigateAppend, navigateUp} from '@/constants/router' +import {navToProfile, navigateAppend} from '@/constants/router' import {useCurrentUserState} from '@/stores/current-user' import type {useTrackerState} from '@/stores/tracker' +import {normalizeProofUsername} from '@/profile/proof-utils' -type ProveGenericParams = { +export type ProveGenericParams = { logoBlack: T.Tracker.SiteIconSet logoFull: T.Tracker.SiteIconSet title: string @@ -39,62 +38,18 @@ export const toProveGenericParams = (p: T.RPCGen.ProveParameters): T.Immutable

-const initialStore: Store = { - blockUserModal: undefined, - errorCode: undefined, - errorText: '', - pgpEmail1: '', - pgpEmail2: '', - pgpEmail3: '', - pgpErrorEmail1: false, - pgpErrorEmail2: false, - pgpErrorEmail3: false, - pgpErrorText: '', - pgpFullName: '', - pgpPublicKey: '', - platformGeneric: undefined, - platformGenericChecking: false, - promptShouldStoreKeyOnServer: false, - proofFound: false, - proofStatus: undefined, - proofText: '', - revokeError: '', - searchShowingSuggestions: false, - sigID: undefined, - username: '', - usernameValid: true, - wotAuthorError: '', } +type Store = T.Immutable<{ +}> + +const initialStore: Store = {} + export type State = Store & { dispatch: { defer: { @@ -110,124 +65,84 @@ export type State = Store & { cancelAddProof?: () => void cancelPgpGen?: () => void finishedWithKeyGen?: (shouldStoreKeyOnServer: boolean) => void - submitUsername?: () => void + submitUsername?: (username: string) => void } addProof: (platform: string, reason: 'appLink' | 'profile') => void - backToProfile: () => void - checkProof: () => void - clearPlatformGeneric: () => void editAvatar: () => void - editProfile: (bio: string, fullname: string, location: string) => void - finishRevoking: () => void - generatePgp: () => void - hideStellar: (h: boolean) => void - recheckProof: (sigID: string) => void + generatePgp: (args: GeneratePgpArgs) => void resetState: () => void showUserProfile: (username: string) => void - submitBlockUser: (username: string) => void - submitBTCAddress: () => void - submitRevokeProof: (proofId: string) => void - submitUnblockUser: (username: string, guiID: string) => void - submitZcashAddress: () => void - updatePgpInfo: (p: { - pgpEmail1?: string - pgpEmail2?: string - pgpEmail3?: string - pgpErrorText?: string - pgpFullName?: string - }) => void - updateUsername: (username: string) => void - uploadAvatar: (filename: string, crop?: T.RPCGen.ImageCropRect) => void } } export const useProfileState = Z.createZustand('profile', (set, get) => { - const clearErrors = (s: Z.WritableDraft) => { - s.errorCode = undefined - s.errorText = '' - s.platformGeneric = undefined - s.platformGenericChecking = false - s.platformGenericParams = undefined - s.platformGenericURL = undefined - s.username = '' - } - const updateUsername = (s: Z.WritableDraft) => { - let username = s.username - let usernameValid = true - - switch (s.platform) { - case 'http': // fallthrough - case 'https': - // Ensure that only the hostname is getting returned, with no - // protocol, port, or path information - username = username - // Remove protocol information (if present) - .replace(/^.*?:\/\//, '') - // Remove port information (if present) - .replace(/:.*/, '') - // Remove path information (if present) - .replace(/\/.*/, '') - break - case 'btc': - { - // A simple check, the server does a fuller check - const legacyFormat = username.search(/^[13][a-km-zA-HJ-NP-Z1-9]{25,34}$/) !== -1 - const segwitFormat = - username.toLowerCase().search(/^(bc1)[qpzry9x8gf2tvdw0s3jn54khce6mua7l]{11,71}$/) !== -1 - usernameValid = legacyFormat || segwitFormat - } - break - default: - } - - s.username = username - s.usernameValid = usernameValid - } - - const _cancelAddProof = () => { + const resetProofCallbacks = () => { set(s => { - clearErrors(s) + s.dispatch.dynamic.afterCheckProof = undefined + s.dispatch.dynamic.cancelAddProof = defaultCancelAddProof + s.dispatch.dynamic.submitUsername = undefined }) } + const defaultCancelAddProof = () => { + resetProofCallbacks() + } - const submitCryptoAddress = (wantedFamily: 'bitcoin' | 'zcash') => { + const resetPgpCallbacks = () => { set(s => { - updateUsername(s) + s.dispatch.dynamic.cancelPgpGen = undefined + s.dispatch.dynamic.finishedWithKeyGen = undefined }) - const f = async () => { - if (!get().usernameValid) { - set(s => { - s.errorText = 'Invalid address format' - s.errorCode = 0 - }) - return - } + } - try { - await T.RPCGen.cryptocurrencyRegisterAddressRpcPromise( - {address: get().username, force: true, wantedFamily}, - S.waitingKeyProfile + const checkProofAndNavigate = async ( + platform: T.More.PlatformsExpandedType, + sigID: T.RPCGen.SigID, + username: string, + proofText: string + ) => { + try { + const {found, status} = await T.RPCGen.proveCheckProofRpcPromise({sigID}, S.waitingKeyProfile) + if (!found && status >= T.RPCGen.ProofStatus.baseHardError) { + navigateAppend( + { + name: 'profilePostProof', + params: { + error: "We couldn't find your proof. Please retry!", + platform, + proofText, + username, + }, + }, + true ) - set(s => { - s.proofFound = true - s.proofStatus = T.RPCGen.ProofStatus.ok + } else { + navigateAppend({ + name: 'profileConfirmOrPending', + params: { + platform, + proofFound: found, + proofStatus: status, + username, + }, }) - navigateAppend('profileConfirmOrPending') - } catch (_error) { - if (_error instanceof RPCError) { - const error = _error - logger.warn('Error making proof') - set(s => { - s.errorText = error.desc - s.errorCode = error.code - }) - } } + } catch { + logger.warn('Error getting proof update') + navigateAppend( + { + name: 'profilePostProof', + params: { + error: "We couldn't verify your proof. Please retry!", + platform, + proofText, + username, + }, + }, + true + ) } - ignorePromise(f()) } - // only let one of these happen at a time let addProofInProgress = false const dispatch: State['dispatch'] = { @@ -236,49 +151,46 @@ export const useProfileState = Z.createZustand('profile', (set, get) => { logger.warn('addProof while one in progress') return } - set(s => { - const maybeNotGeneric = T.More.asPlatformsExpandedType(platform) - clearErrors(s) - s.platform = maybeNotGeneric ?? undefined - s.platformGeneric = maybeNotGeneric ? undefined : platform - updateUsername(s) - }) - const f = async () => { - const service = T.More.asPlatformsExpandedType(platform) - const genericService = service ? null : platform - // Special cases - switch (service) { - case 'dnsOrGenericWebSite': - navigateAppend('profileProveWebsiteChoice') - return - case 'zcash': // fallthrough - case 'btc': - navigateAppend('profileProveEnterUsername') - return - case 'pgp': - navigateAppend('profilePgp') - return - default: - } - addProofInProgress = true + const service = T.More.asPlatformsExpandedType(platform) + const genericService = service ? null : platform - const inputCancelError = { - code: T.RPCGen.StatusCode.scinputcanceled, - desc: 'Cancel Add Proof', - } - set(s => { - s.sigID = undefined + switch (service) { + case 'dnsOrGenericWebSite': + navigateAppend('profileProveWebsiteChoice') + return + case 'zcash': + case 'btc': + navigateAppend({name: 'profileProveEnterUsername', params: {platform: service}}) + return + case 'pgp': + navigateAppend('profilePgp') + return + default: + break + } + + addProofInProgress = true + + const inputCancelError = { + code: T.RPCGen.StatusCode.scinputcanceled, + desc: 'Cancel Add Proof', + } + + let canceled = false + let currentUsername = '' + let currentGenericParams = makeProveGenericParams() + let currentProofText = '' + + const loadAfter = () => + get().dispatch.defer.onTracker2Load?.({ + assertion: useCurrentUserState.getState().username, + guiID: generateGUIID(), + inTracker: false, + reason: '', }) - let canceled = false - const loadAfter = () => - get().dispatch.defer.onTracker2Load?.({ - assertion: useCurrentUserState.getState().username, - guiID: generateGUIID(), - inTracker: false, - reason: '', - }) + const f = async () => { try { const {sigID} = await T.RPCGen.proveStartProofRpcListener({ customResponseIncomingCallMap: { @@ -288,11 +200,7 @@ export const useProfileState = Z.createZustand('profile', (set, get) => { return } response.result() - set(s => { - s.platformGenericChecking = true - }) }, - // service calls in when it polls to give us an opportunity to cancel 'keybase.1.proveUi.continueChecking': (_, response) => canceled ? response.result(false) : response.result(true), 'keybase.1.proveUi.okToCheck': (_, response) => response.result(true), @@ -309,25 +217,37 @@ export const useProfileState = Z.createZustand('profile', (set, get) => { response.result() }) s.dispatch.dynamic.cancelAddProof = wrapErrors(() => { - set(s => { - s.dispatch.dynamic.cancelAddProof = _cancelAddProof - }) - _cancelAddProof() + resetProofCallbacks() canceled = true response.error(inputCancelError) }) }) - if (service) { - set(s => { - s.proofText = proof + + if (service && proof) { + currentProofText = proof + navigateAppend({ + name: 'profilePostProof', + params: { + platform: service, + proofText: proof, + username: currentUsername, + }, }) - navigateAppend('profilePostProof') } else if (proof) { - set(s => { - s.platformGenericURL = proof - }) + navigateAppend( + { + name: 'profileGenericEnterUsername', + params: { + genericParams: currentGenericParams, + proofUrl: proof, + service: genericService ?? '', + username: currentUsername, + }, + }, + true + ) openURL(proof) - get().dispatch.checkProof() + get().dispatch.dynamic.afterCheckProof?.() } }, 'keybase.1.proveUi.preProofWarning': (_, response) => response.result(true), @@ -338,44 +258,48 @@ export const useProfileState = Z.createZustand('profile', (set, get) => { response.error(inputCancelError) return } - const clear = () => { - set(s => { - s.errorText = '' - s.errorCode = undefined - }) - } set(s => { s.dispatch.dynamic.cancelAddProof = wrapErrors(() => { - clear() - set(s => { - s.dispatch.dynamic.cancelAddProof = _cancelAddProof - }) - _cancelAddProof() + resetProofCallbacks() canceled = true response.error(inputCancelError) }) - s.dispatch.dynamic.submitUsername = wrapErrors(() => { - clear() + s.dispatch.dynamic.submitUsername = wrapErrors((username: string) => { + const {normalized} = normalizeProofUsername(service, username) + currentUsername = normalized set(s => { - updateUsername(s) s.dispatch.dynamic.submitUsername = undefined }) - response.result(get().username) + response.result(normalized) }) }) - if (prevError) { - set(s => { - s.errorText = prevError.desc - s.errorCode = prevError.code - }) - } + if (service) { - navigateAppend('profileProveEnterUsername') + navigateAppend( + { + name: 'profileProveEnterUsername', + params: { + error: prevError?.desc, + platform: service, + username: currentUsername || undefined, + }, + }, + true + ) } else if (genericService && parameters) { - set(s => { - s.platformGenericParams = T.castDraft(toProveGenericParams(parameters)) - }) - navigateAppend('profileGenericEnterUsername') + currentGenericParams = toProveGenericParams(parameters) + navigateAppend( + { + name: 'profileGenericEnterUsername', + params: { + error: prevError?.desc, + genericParams: currentGenericParams, + service: genericService, + username: currentUsername || undefined, + }, + }, + true + ) } }, }, @@ -386,34 +310,50 @@ export const useProfileState = Z.createZustand('profile', (set, get) => { params: { auto: false, force: true, - promptPosted: !!genericService, // proof protocol extended slightly for generic proofs + promptPosted: !!genericService, service: platform, username: '', }, waitingKey: S.waitingKeyProfile, }) - set(s => { - s.sigID = sigID - }) + logger.info('Start Proof done: ', sigID) - if (!genericService) { - get().dispatch.checkProof() - } loadAfter() - if (genericService) { - set(s => { - s.platformGenericChecking = false - }) + + if (service) { + await checkProofAndNavigate(service, sigID, currentUsername, currentProofText) + } else { + navigateAppend( + { + name: 'profileGenericProofResult', + params: { + genericParams: currentGenericParams, + username: currentUsername, + }, + }, + true + ) } } catch (_error) { + loadAfter() if (_error instanceof RPCError) { const error = _error logger.warn('Error making proof') - loadAfter() - set(s => { - s.errorText = error.desc - s.errorCode = error.code - }) + + if (genericService) { + navigateAppend( + { + name: 'profileGenericProofResult', + params: { + error: error.desc || 'Failed to verify proof', + genericParams: currentGenericParams, + username: currentUsername, + }, + }, + true + ) + } + if (error.code === T.RPCGen.StatusCode.scgeneric && reason === 'appLink') { navigateAppend({ name: 'keybaseLinkError', @@ -424,71 +364,14 @@ export const useProfileState = Z.createZustand('profile', (set, get) => { }) } } - if (genericService) { - set(s => { - s.platformGenericChecking = false - }) - } } finally { addProofInProgress = false - set(s => { - s.dispatch.dynamic.cancelAddProof = _cancelAddProof - s.dispatch.dynamic.afterCheckProof = undefined - s.dispatch.dynamic.cancelPgpGen = undefined - s.dispatch.dynamic.submitUsername = undefined - }) + resetProofCallbacks() + resetPgpCallbacks() } } ignorePromise(f()) }, - backToProfile: () => { - clearModals() - setTimeout(() => { - get().dispatch.showUserProfile(useCurrentUserState.getState().username) - }, 100) - }, - checkProof: () => { - set(s => { - clearErrors(s) - }) - const f = async () => { - const sigID = get().sigID - const isGeneric = !!get().platformGeneric - if (!sigID) { - return - } - try { - const {found, status} = await T.RPCGen.proveCheckProofRpcPromise({sigID}, S.waitingKeyProfile) - // Values higher than baseHardError are hard errors, below are soft errors (could eventually be resolved by doing nothing) - if (!found && status >= T.RPCGen.ProofStatus.baseHardError) { - set(s => { - s.errorText = "We couldn't find your proof. Please retry!" - }) - } else { - set(s => { - s.errorText = '' - s.proofFound = found - s.proofStatus = status - }) - if (!isGeneric) { - navigateAppend('profileConfirmOrPending') - } - } - } catch { - logger.warn('Error getting proof update') - set(s => { - s.errorText = "We couldn't verify your proof. Please retry!" - }) - } - } - ignorePromise(f()) - get().dispatch.dynamic.afterCheckProof?.() - }, - clearPlatformGeneric: () => { - set(s => { - clearErrors(s) - }) - }, defer: { onTracker2GetDetails: () => { throw new Error('onTracker2GetDetails not implemented') @@ -504,45 +387,25 @@ export const useProfileState = Z.createZustand('profile', (set, get) => { }, }, dynamic: { - cancelAddProof: _cancelAddProof, + cancelAddProof: defaultCancelAddProof, cancelPgpGen: undefined, finishedWithKeyGen: undefined, + submitUsername: undefined, }, editAvatar: () => { throw new Error('This is overloaded by platform specific') }, - editProfile: (bio, fullName, location) => { - const f = async () => { - await T.RPCGen.userProfileEditRpcPromise({bio, fullName, location}, S.waitingKeyTracker) - get().dispatch.showUserProfile(useCurrentUserState.getState().username) - } - ignorePromise(f()) - }, - finishRevoking: () => { - const username = useCurrentUserState.getState().username - get().dispatch.showUserProfile(username) - get().dispatch.defer.onTracker2Load?.({ - assertion: username, - guiID: generateGUIID(), - inTracker: false, - reason: '', - }) - set(s => { - s.revokeError = '' - }) - }, - generatePgp: () => { + generatePgp: args => { const f = async () => { let canceled = false - const {pgpEmail1, pgpEmail2, pgpEmail3, pgpFullName} = get() - const ids = [pgpEmail1, pgpEmail2, pgpEmail3].filter(Boolean).map(email => ({ + let pgpKeyString = 'Error getting public key...' + const ids = [args.pgpEmail1, args.pgpEmail2, args.pgpEmail3].filter(Boolean).map(email => ({ comment: '', email: email || '', - username: pgpFullName || '', + username: args.pgpFullName || '', })) navigateAppend('profileGenerate') - // We allow the UI to cancel this call. Just stash this intention and nav away and response with an error to the rpc set(s => { s.dispatch.dynamic.cancelPgpGen = wrapErrors(() => { canceled = true @@ -556,16 +419,16 @@ export const useProfileState = Z.createZustand('profile', (set, get) => { if (canceled) { response.error({code: T.RPCGen.StatusCode.scinputcanceled, desc: 'Input canceled'}) } else { + pgpKeyString = key.key response.result() - set(s => { - s.pgpPublicKey = key.key - }) } }, 'keybase.1.pgpUi.shouldPushPrivate': ({prompt}, response) => { - navigateAppend('profileFinished') + navigateAppend({ + name: 'profileFinished', + params: {pgpKeyString, promptShouldStoreKeyOnServer: prompt}, + }) set(s => { - s.promptShouldStoreKeyOnServer = prompt s.dispatch.dynamic.finishedWithKeyGen = wrapErrors((shouldStoreKeyOnServer: boolean) => { set(s => { s.dispatch.dynamic.finishedWithKeyGen = undefined @@ -582,185 +445,36 @@ export const useProfileState = Z.createZustand('profile', (set, get) => { if (!(error instanceof RPCError)) { return } - // did we cancel? if (error.code !== T.RPCGen.StatusCode.scinputcanceled) { throw error } - } - set(s => { - s.dispatch.dynamic.cancelPgpGen = undefined - s.dispatch.dynamic.finishedWithKeyGen = undefined - }) - } - ignorePromise(f()) - }, - hideStellar: hidden => { - const f = async () => { - try { - await T.RPCGen.apiserverPostRpcPromise( - { - args: [{key: 'hidden', value: hidden ? '1' : '0'}], - endpoint: 'stellar/hidden', - }, - S.waitingKeyTracker - ) - } catch (e) { - logger.warn('Error setting Stellar hidden:', e) + } finally { + resetPgpCallbacks() } } ignorePromise(f()) }, - recheckProof: sigID => { - set(s => { - s.errorCode = undefined - s.errorText = '' - }) - const f = async () => { - await T.RPCGen.proveCheckProofRpcPromise({sigID}, S.waitingKeyProfile) - get().dispatch.defer.onTracker2ShowUser?.(useCurrentUserState.getState().username, false) - } - ignorePromise(f()) - }, resetState: () => { set(s => ({ ...s, ...initialStore, - dispatch: s.dispatch, + dispatch: { + ...s.dispatch, + dynamic: { + afterCheckProof: undefined, + cancelAddProof: defaultCancelAddProof, + cancelPgpGen: undefined, + finishedWithKeyGen: undefined, + submitUsername: undefined, + }, + }, })) }, showUserProfile: username => { navToProfile(username) }, - submitBTCAddress: () => { - submitCryptoAddress('bitcoin') - }, - submitBlockUser: username => { - set(s => { - s.blockUserModal = 'waiting' - }) - const f = async () => { - try { - await T.RPCGen.userBlockUserRpcPromise({username}) - set(s => { - s.blockUserModal = undefined - }) - get().dispatch.defer.onTracker2Load?.({ - assertion: username, - guiID: generateGUIID(), - inTracker: false, - reason: '', - }) - } catch (_error) { - if (!(_error instanceof RPCError)) { - return - } - const error = _error - logger.warn(`Error blocking user ${username}`, error) - set(s => { - s.blockUserModal = {error: error.desc || `There was an error blocking ${username}.`} - }) - } - } - ignorePromise(f()) - }, - submitRevokeProof: proofId => { - const f = async () => { - const you = get().dispatch.defer.onTracker2GetDetails?.(useCurrentUserState.getState().username) - if (!you?.assertions) return - const proof = [...you.assertions.values()].find(a => a.sigID === proofId) - if (!proof) return - - if (proof.type === 'pgp') { - try { - await T.RPCGen.revokeRevokeKeyRpcPromise({keyID: proof.kid}, S.waitingKeyProfile) - } catch (e) { - logger.info('error in dropping pgp key', e) - set(s => { - s.revokeError = `Error in dropping Pgp Key: ${String(e)}` - }) - } - } else { - try { - await T.RPCGen.revokeRevokeSigsRpcPromise({sigIDQueries: [proofId]}, S.waitingKeyProfile) - get().dispatch.finishRevoking() - } catch (error) { - logger.warn(`Error when revoking proof ${proofId}`, error) - set(s => { - s.revokeError = 'There was an error revoking your proof. You can click the button to try again.' - }) - } - } - } - ignorePromise(f()) - }, - submitUnblockUser: (username, guiID) => { - const f = async () => { - try { - await T.RPCGen.userUnblockUserRpcPromise({username}) - get().dispatch.defer.onTracker2Load?.({ - assertion: username, - guiID: generateGUIID(), - inTracker: false, - reason: '', - }) - } catch (_error) { - if (!(_error instanceof RPCError)) { - return - } - const error = _error - logger.warn(`Error unblocking user ${username}`, error) - get().dispatch.defer.onTracker2UpdateResult?.( - guiID, - 'error', - `Failed to unblock ${username}: ${error.desc}` - ) - } - } - ignorePromise(f()) - }, - submitZcashAddress: () => { - submitCryptoAddress('zcash') - }, - updatePgpInfo: p => { - set(s => { - s.pgpEmail1 = p.pgpEmail1 ?? s.pgpEmail1 - s.pgpEmail2 = p.pgpEmail2 ?? s.pgpEmail2 - s.pgpEmail3 = p.pgpEmail3 ?? s.pgpEmail3 - const valid1 = Validators.isValidEmail(s.pgpEmail1) - const valid2 = s.pgpEmail2 && Validators.isValidEmail(s.pgpEmail2) - const valid3 = s.pgpEmail3 && Validators.isValidEmail(s.pgpEmail3) - s.pgpErrorEmail1 = !!valid1 - s.pgpErrorEmail2 = !!valid2 - s.pgpErrorEmail3 = !!valid3 - s.pgpErrorText = Validators.isValidName(s.pgpFullName) || valid1 || valid2 || valid3 - s.pgpFullName = p.pgpFullName || s.pgpFullName - }) - }, - updateUsername: username => { - set(s => { - s.username = username - updateUsername(s) - }) - }, - uploadAvatar: (filename, crop) => { - const f = async () => { - try { - await T.RPCGen.userUploadUserAvatarRpcPromise( - {crop: fixCrop(crop), filename}, - S.waitingKeyProfileUploadAvatar - ) - navigateUp() - } catch (error) { - if (!(error instanceof RPCError)) { - return - } - // error displayed in component - logger.warn(`Error uploading user avatar: ${error.message}`) - } - } - ignorePromise(f()) - }, } + return { ...initialStore, dispatch, diff --git a/shared/stores/tests/profile.test.ts b/shared/stores/tests/profile.test.ts index 6ff55055ac05..a74e4d091521 100644 --- a/shared/stores/tests/profile.test.ts +++ b/shared/stores/tests/profile.test.ts @@ -1,89 +1,66 @@ /// +import {validatePgpInfo} from '@/profile/pgp/validation' +import {normalizeProofUsername} from '@/profile/proof-utils' import {resetAllStores} from '@/util/zustand' -import {useCurrentUserState} from '../current-user' import {useProfileState} from '../profile' beforeEach(() => { resetAllStores() - useCurrentUserState.getState().dispatch.setBootstrap({ - deviceID: 'device-id', - deviceName: 'test-device', - uid: 'uid', - username: 'alice', - }) }) afterEach(() => { resetAllStores() }) -test('updateUsername normalizes http proofs to the bare hostname', () => { - useProfileState.setState({platform: 'https'} as never) - - useProfileState.getState().dispatch.updateUsername('https://example.com:3000/path/to/proof') +test('normalizeProofUsername normalizes http proofs to the bare hostname', () => { + const result = normalizeProofUsername('https', 'https://example.com:3000/path/to/proof') - const state = useProfileState.getState() - expect(state.username).toBe('example.com') - expect(state.usernameValid).toBe(true) + expect(result.normalized).toBe('example.com') + expect(result.valid).toBe(true) }) -test('updateUsername validates bitcoin addresses', () => { - useProfileState.setState({platform: 'btc'} as never) - - useProfileState.getState().dispatch.updateUsername('not-a-btc-address') - expect(useProfileState.getState().usernameValid).toBe(false) - - useProfileState.getState().dispatch.updateUsername('1BoatSLRHtKNngkdXEeobR76b53LETtpyT') - expect(useProfileState.getState().usernameValid).toBe(true) +test('normalizeProofUsername validates bitcoin addresses', () => { + expect(normalizeProofUsername('btc', 'not-a-btc-address').valid).toBe(false) + expect(normalizeProofUsername('btc', '1BoatSLRHtKNngkdXEeobR76b53LETtpyT').valid).toBe(true) }) -test('clearPlatformGeneric clears proof flow errors and resetState restores defaults', () => { - useProfileState.setState({ - errorCode: 42, - errorText: 'boom', - platformGeneric: 'dns', - platformGenericChecking: true, - platformGenericParams: {buttonLabel: 'Add', logoBlack: [], logoFull: [], subtext: '', suffix: '', title: ''}, - platformGenericURL: 'https://keybase.io', - proofFound: true, - username: 'alice', - } as never) - - useProfileState.getState().dispatch.clearPlatformGeneric() +test('validatePgpInfo derives validation errors from local form state', () => { + let result = validatePgpInfo({ + pgpEmail1: 'bad-email', + pgpEmail2: 'also-bad', + pgpEmail3: '', + pgpFullName: 'Alice', + }) - let state = useProfileState.getState() - expect(state.errorCode).toBeUndefined() - expect(state.errorText).toBe('') - expect(state.platformGeneric).toBeUndefined() - expect(state.platformGenericChecking).toBe(false) - expect(state.platformGenericParams).toBeUndefined() - expect(state.platformGenericURL).toBeUndefined() - expect(state.username).toBe('') + expect(result.pgpErrorEmail1).toBe(true) + expect(result.pgpErrorEmail2).toBe(true) + expect(result.pgpErrorEmail3).toBe(false) - useProfileState.setState({revokeError: 'still here', username: 'bob'} as never) - useProfileState.getState().dispatch.resetState() + result = validatePgpInfo({ + pgpEmail1: 'alice@keybase.io', + pgpEmail2: '', + pgpEmail3: '', + pgpFullName: 'Alice', + }) - state = useProfileState.getState() - expect(state.revokeError).toBe('') - expect(state.username).toBe('') - expect(state.dispatch).toBeDefined() + expect(result.pgpErrorEmail1).toBe(false) + expect(result.pgpErrorText).toBe('') }) -test('updatePgpInfo stores fields and derives validation errors', () => { - useProfileState.getState().dispatch.updatePgpInfo({ - pgpEmail1: 'bad-email', - pgpEmail2: 'also-bad', - pgpFullName: 'Alice', +test('resetState clears dynamic profile callbacks and preserves the default cancel hook', () => { + useProfileState.setState(s => { + s.dispatch.dynamic.afterCheckProof = () => {} + s.dispatch.dynamic.cancelPgpGen = () => {} + s.dispatch.dynamic.finishedWithKeyGen = () => {} + s.dispatch.dynamic.submitUsername = () => {} }) - let state = useProfileState.getState() - expect(state.pgpEmail1).toBe('bad-email') - expect(state.pgpEmail2).toBe('also-bad') - expect(state.pgpErrorEmail1).toBe(true) - expect(state.pgpErrorEmail2).toBe(true) + useProfileState.getState().dispatch.resetState() - useProfileState.getState().dispatch.updatePgpInfo({pgpEmail1: 'alice@keybase.io'}) - state = useProfileState.getState() - expect(state.pgpEmail1).toBe('alice@keybase.io') - expect(state.pgpErrorEmail1).toBe(false) + const state = useProfileState.getState() + expect(state.dispatch.dynamic.afterCheckProof).toBeUndefined() + expect(state.dispatch.dynamic.cancelPgpGen).toBeUndefined() + expect(state.dispatch.dynamic.finishedWithKeyGen).toBeUndefined() + expect(state.dispatch.dynamic.submitUsername).toBeUndefined() + expect(typeof state.dispatch.dynamic.cancelAddProof).toBe('function') }) diff --git a/shared/tracker/assertion.tsx b/shared/tracker/assertion.tsx index e6f52e841b3b..03652e88c825 100644 --- a/shared/tracker/assertion.tsx +++ b/shared/tracker/assertion.tsx @@ -11,6 +11,7 @@ import {useColorScheme} from 'react-native' import * as Tracker from '@/stores/tracker' import {useTrackerState} from '@/stores/tracker' import {useProfileState} from '@/stores/profile' +import {generateGUIID} from '@/constants/utils' type OwnProps = { isSuggestion?: boolean @@ -75,16 +76,29 @@ const Container = (ownProps: OwnProps) => { const {color, metas: _metas, proofURL, sigID, siteIcon, stellarHidden, notAUser} = data const {siteIconDarkmode, siteIconFull, siteIconFullDarkmode, siteURL, state, timestamp, type, value} = data const addProof = useProfileState(s => s.dispatch.addProof) - const hideStellar = useProfileState(s => s.dispatch.hideStellar) - const recheckProof = useProfileState(s => s.dispatch.recheckProof) + const showUserProfile = useProfileState(s => s.dispatch.showUserProfile) + const loadProfile = useTrackerState(s => s.dispatch.load) + const hideStellar = C.useRPC(T.RPCGen.apiserverPostRpcPromise) + const recheckProof = C.useRPC(T.RPCGen.proveCheckProofRpcPromise) const _onCreateProof = () => { addProof(type, 'profile') } const onHideStellar = (hidden: boolean) => { - hideStellar(hidden) + hideStellar( + [{args: [{key: 'hidden', value: hidden ? '1' : '0'}], endpoint: 'stellar/hidden'}, C.waitingKeyTracker], + () => {}, + () => {} + ) } const onRecheck = () => { - recheckProof(sigID) + recheckProof( + [{sigID}, C.waitingKeyProfile], + () => { + showUserProfile(ownProps.username) + loadProfile({assertion: ownProps.username, guiID: generateGUIID(), inTracker: false, reason: ''}) + }, + () => {} + ) } const navigateAppend = C.useRouterState(s => s.dispatch.navigateAppend) diff --git a/skill/zustand-store-pruning/references/store-checklist.md b/skill/zustand-store-pruning/references/store-checklist.md index 4d6033766cd2..d9792a54dd7a 100644 --- a/skill/zustand-store-pruning/references/store-checklist.md +++ b/skill/zustand-store-pruning/references/store-checklist.md @@ -25,8 +25,8 @@ Status: - [x] `modal-header` - [x] `notifications` kept notification-driven badge aggregation, tray/widget badge state, and engine/init badge plumbing in store - [x] `people` kept only `homeUIRefresh` / route-leave `markViewed` plumbing in store; moved fetched People rows, follow suggestions, skip/dismiss RPCs, and resend-email banner state into the People feature layer -- [ ] `pinentry` -- [ ] `profile` +- [x] `pinentry` kept daemon passphrase callback coordination, remote-window prompt state, and submit/cancel closures in store +- [x] `profile` kept proof/PGP listener callbacks plus shared navigation hooks in store; moved visible proof/PGP/revoke state and one-screen RPCs into route params or owning components - [x] `recover-password` kept only session callbacks plus `resetEmailSent`; moved recover-flow display state and navigation context into route params - [ ] `settings` - [ ] `settings-chat` From 24a4dbd80c81858dc5f6ec7f5adade53cdbdbcac Mon Sep 17 00:00:00 2001 From: Chris Nojima Date: Fri, 27 Mar 2026 15:34:15 -0400 Subject: [PATCH 02/14] WIP --- shared/profile/confirm-or-pending.tsx | 19 ++++-------- shared/profile/generic/enter-username.tsx | 29 ++++++++----------- shared/profile/generic/result.tsx | 17 ++++------- shared/profile/pgp/finished/index.desktop.tsx | 11 ++----- shared/profile/post-proof.tsx | 29 ++++++------------- shared/profile/proof-utils.ts | 2 +- shared/profile/prove-enter-username.tsx | 13 +++------ 7 files changed, 40 insertions(+), 80 deletions(-) diff --git a/shared/profile/confirm-or-pending.tsx b/shared/profile/confirm-or-pending.tsx index 9dc09b964177..b32f629ab4bf 100644 --- a/shared/profile/confirm-or-pending.tsx +++ b/shared/profile/confirm-or-pending.tsx @@ -7,18 +7,13 @@ import {useCurrentUserState} from '@/stores/current-user' import * as C from '@/constants' type Props = { - route: { - params: { - platform: T.More.PlatformsExpandedType - proofFound: boolean - proofStatus?: T.RPCGen.ProofStatus - username: string - } - } + platform: T.More.PlatformsExpandedType + proofFound: boolean + proofStatus?: T.RPCGen.ProofStatus + username: string } -const ConfirmOrPending = ({route}: Props) => { - const {platform, proofFound, proofStatus, username} = route.params +const ConfirmOrPending = ({platform, proofFound, proofStatus, username}: Props) => { const showUserProfile = useProfileState(s => s.dispatch.showUserProfile) const currentUsername = useCurrentUserState(s => s.username) const clearModals = C.useRouterState(s => s.dispatch.clearModals) @@ -27,10 +22,6 @@ const ConfirmOrPending = ({route}: Props) => { const isPending = !isGood && !proofFound && !!proofStatus && proofStatus <= T.RPCGen.ProofStatus.baseHardError - if (!platform) { - throw new Error('No platform passed to confirm or pending container') - } - const platformIconOverlayColor = isGood ? Kb.Styles.globalColors.green : Kb.Styles.globalColors.greyDark const onCancel = () => { clearModals() diff --git a/shared/profile/generic/enter-username.tsx b/shared/profile/generic/enter-username.tsx index f7152f7ccaf3..b211ea75f609 100644 --- a/shared/profile/generic/enter-username.tsx +++ b/shared/profile/generic/enter-username.tsx @@ -7,25 +7,20 @@ import {SiteIcon} from './shared' import type * as T from '@/constants/types' type Props = { - route: { - params: { - error?: string - genericParams: ProveGenericParams - proofUrl?: string - service: string - username?: string - } - } + error?: string + genericParams: ProveGenericParams + proofUrl?: string + service: string + username?: string } -const ConnectedEnterUsername = ({route}: Props) => { - const {error = '', genericParams, proofUrl, username: routeUsername = ''} = route.params - const serviceIcon = genericParams.logoBlack ?? [] - const serviceIconFull = genericParams.logoFull ?? [] - const serviceName = genericParams.title ?? '' - const serviceSub = genericParams.subtext ?? '' - const serviceSuffix = genericParams.suffix ?? '' - const submitButtonLabel = genericParams.buttonLabel ?? 'Submit' +const ConnectedEnterUsername = ({error = '', genericParams, proofUrl, username: routeUsername = ''}: Props) => { + const serviceIcon = genericParams.logoBlack + const serviceIconFull = genericParams.logoFull + const serviceName = genericParams.title + const serviceSub = genericParams.subtext + const serviceSuffix = genericParams.suffix + const submitButtonLabel = genericParams.buttonLabel const unreachable = !!proofUrl const cancelAddProof = useProfileState(s => s.dispatch.dynamic.cancelAddProof) diff --git a/shared/profile/generic/result.tsx b/shared/profile/generic/result.tsx index a3a35adfb552..b0fc47bdb035 100644 --- a/shared/profile/generic/result.tsx +++ b/shared/profile/generic/result.tsx @@ -6,19 +6,14 @@ import {useCurrentUserState} from '@/stores/current-user' import type {ProveGenericParams} from '@/stores/profile' type Props = { - route: { - params: { - error?: string - genericParams: ProveGenericParams - username: string - } - } + error?: string + genericParams: ProveGenericParams + username: string } -const GenericResult = ({route}: Props) => { - const {error = '', genericParams, username} = route.params - const proofUsername = username + (genericParams.suffix ?? '@unknown') - const serviceIcon = genericParams.logoFull ?? [] +const GenericResult = ({error = '', genericParams, username}: Props) => { + const proofUsername = username + genericParams.suffix + const serviceIcon = genericParams.logoFull const showUserProfile = useProfileState(s => s.dispatch.showUserProfile) const currentUsername = useCurrentUserState(s => s.username) const clearModals = C.useRouterState(s => s.dispatch.clearModals) diff --git a/shared/profile/pgp/finished/index.desktop.tsx b/shared/profile/pgp/finished/index.desktop.tsx index d984ee7ea5fd..734dab3919a5 100644 --- a/shared/profile/pgp/finished/index.desktop.tsx +++ b/shared/profile/pgp/finished/index.desktop.tsx @@ -77,16 +77,11 @@ const styles = Kb.Styles.styleSheetCreate( ) type ContainerProps = { - route: { - params: { - pgpKeyString: string - promptShouldStoreKeyOnServer: boolean - } - } + pgpKeyString: string + promptShouldStoreKeyOnServer: boolean } -const Container = ({route}: ContainerProps) => { - const {pgpKeyString, promptShouldStoreKeyOnServer} = route.params +const Container = ({pgpKeyString, promptShouldStoreKeyOnServer}: ContainerProps) => { const finishedWithKeyGen = useProfileState(s => s.dispatch.dynamic.finishedWithKeyGen) const clearModals = C.useRouterState(s => s.dispatch.clearModals) diff --git a/shared/profile/post-proof.tsx b/shared/profile/post-proof.tsx index df270d3fd367..85c09661bc8f 100644 --- a/shared/profile/post-proof.tsx +++ b/shared/profile/post-proof.tsx @@ -9,28 +9,17 @@ import {useConfigState} from '@/stores/config' import type * as T from '@/constants/types' type Props = { - route: { - params: { - error?: string - platform: T.More.PlatformsExpandedType - proofText: string - username: string - } - } + error?: string + platform: T.More.PlatformsExpandedType + proofText: string + username: string } -const Container = ({route}: Props) => { - const {error, platform, username} = route.params - let {proofText} = route.params +const Container = ({error, platform, username, proofText: initialProofText}: Props) => { + let proofText = initialProofText const cancelAddProof = useProfileState(s => s.dispatch.dynamic.cancelAddProof) const afterCheckProof = useProfileState(s => s.dispatch.dynamic.afterCheckProof) - if ( - !platform || - platform === 'zcash' || - platform === 'btc' || - platform === 'dnsOrGenericWebSite' || - platform === 'pgp' - ) { + if (platform === 'zcash' || platform === 'btc' || platform === 'dnsOrGenericWebSite' || platform === 'pgp') { throw new Error(`Invalid profile platform in PostProofContainer: ${platform || ''}`) } @@ -39,7 +28,7 @@ const Container = ({route}: Props) => { switch (platform) { case 'twitter': openLinkBeforeSubmit = true - url = proofText ? `https://twitter.com/home?status=${proofText || ''}` : '' + url = proofText ? `https://twitter.com/home?status=${proofText}` : '' break case 'github': openLinkBeforeSubmit = true @@ -53,7 +42,7 @@ const Container = ({route}: Props) => { break case 'hackernews': openLinkBeforeSubmit = true - url = `https://news.ycombinator.com/user?id=${username || ''}` + url = `https://news.ycombinator.com/user?id=${username}` break default: break diff --git a/shared/profile/proof-utils.ts b/shared/profile/proof-utils.ts index dc522449e014..0fa1223c4567 100644 --- a/shared/profile/proof-utils.ts +++ b/shared/profile/proof-utils.ts @@ -1,4 +1,4 @@ -import * as T from '@/constants/types' +import type * as T from '@/constants/types' const isValidBitcoinAddress = (username: string) => { const legacyFormat = username.search(/^[13][a-km-zA-HJ-NP-Z1-9]{25,34}$/) !== -1 diff --git a/shared/profile/prove-enter-username.tsx b/shared/profile/prove-enter-username.tsx index b00436d8cb37..90663b7c0603 100644 --- a/shared/profile/prove-enter-username.tsx +++ b/shared/profile/prove-enter-username.tsx @@ -7,17 +7,12 @@ import type * as T from '@/constants/types' import {normalizeProofUsername} from './proof-utils' type Props = { - route: { - params: { - error?: string - platform: T.More.PlatformsExpandedType - username?: string - } - } + error?: string + platform: T.More.PlatformsExpandedType + username?: string } -const Container = ({route}: Props) => { - const {error: routeError, platform, username: initialUsername = ''} = route.params +const Container = ({error: routeError, platform, username: initialUsername = ''}: Props) => { const cancelAddProof = useProfileState(s => s.dispatch.dynamic.cancelAddProof) const submitUsername = useProfileState(s => s.dispatch.dynamic.submitUsername) const registerCryptoAddress = C.useRPC(T.RPCGen.cryptocurrencyRegisterAddressRpcPromise) From 8c0027fb26227e2f2f9600fe36386e52235196b5 Mon Sep 17 00:00:00 2001 From: chrisnojima Date: Fri, 27 Mar 2026 15:34:30 -0400 Subject: [PATCH 03/14] WIP --- shared/profile/prove-enter-username.tsx | 7 ++++-- shared/profile/revoke.tsx | 30 ++++++++++++------------- shared/tracker/assertion.tsx | 6 +++-- 3 files changed, 23 insertions(+), 20 deletions(-) diff --git a/shared/profile/prove-enter-username.tsx b/shared/profile/prove-enter-username.tsx index 90663b7c0603..6c87d1f98dfd 100644 --- a/shared/profile/prove-enter-username.tsx +++ b/shared/profile/prove-enter-username.tsx @@ -3,7 +3,7 @@ import {useProfileState} from '@/stores/profile' import * as React from 'react' import * as Kb from '@/common-adapters' import Modal from './modal' -import type * as T from '@/constants/types' +import * as T from '@/constants/types' import {normalizeProofUsername} from './proof-utils' type Props = { @@ -31,7 +31,10 @@ const Container = ({error: routeError, platform, username: initialUsername = ''} const navigateAppend = C.useRouterState(s => s.dispatch.navigateAppend) const _onSubmit = (input: string, platform?: string) => { - const {normalized, valid} = normalizeProofUsername(platform as T.More.PlatformsExpandedType | undefined, input) + const {normalized, valid} = normalizeProofUsername( + platform as T.More.PlatformsExpandedType | undefined, + input + ) if (platform === 'btc') { if (!valid) { diff --git a/shared/profile/revoke.tsx b/shared/profile/revoke.tsx index f57032754798..e6d326b7cd4c 100644 --- a/shared/profile/revoke.tsx +++ b/shared/profile/revoke.tsx @@ -5,7 +5,7 @@ import * as React from 'react' import capitalize from 'lodash/capitalize' import {subtitle as platformSubtitle} from '@/util/platforms' import {SiteIcon} from './generic/shared' -import type * as T from '@/constants/types' +import * as T from '@/constants/types' import Modal from './modal' import {useCurrentUserState} from '@/stores/current-user' import {useTrackerState} from '@/stores/tracker' @@ -42,29 +42,27 @@ const RevokeProof = (ownProps: OwnProps) => { return } if (proof.type === 'pgp') { - revokeKey( - [{keyID: proof.kid}, C.waitingKeyProfile], - onSuccess, - error => { - setErrorMessage(`Error in dropping Pgp Key: ${error.message}`) - } - ) + revokeKey([{keyID: proof.kid}, C.waitingKeyProfile], onSuccess, error => { + setErrorMessage(`Error in dropping Pgp Key: ${error.message}`) + }) return } - revokeSigs( - [{sigIDQueries: [proofId]}, C.waitingKeyProfile], - onSuccess, - () => { - setErrorMessage('There was an error revoking your proof. You can click the button to try again.') - } - ) + revokeSigs([{sigIDQueries: [proofId]}, C.waitingKeyProfile], onSuccess, () => { + setErrorMessage('There was an error revoking your proof. You can click the button to try again.') + }) } const platformHandleSubtitle = platformSubtitle(platform) return ( {!!errorMessage && ( - + {errorMessage} diff --git a/shared/tracker/assertion.tsx b/shared/tracker/assertion.tsx index 03652e88c825..b325668141ee 100644 --- a/shared/tracker/assertion.tsx +++ b/shared/tracker/assertion.tsx @@ -2,7 +2,7 @@ import type * as React from 'react' import * as C from '@/constants' import {useConfigState} from '@/stores/config' import {useCurrentUserState} from '@/stores/current-user' -import type * as T from '@/constants/types' +import * as T from '@/constants/types' import {openURL as openUrl} from '@/util/misc' import * as Kb from '@/common-adapters' import {SiteIcon} from '@/profile/generic/shared' @@ -505,7 +505,9 @@ const Value = (p: { } const HoverOpacity = (p: {children: React.ReactNode}) => ( - {p.children} + + {p.children} + ) type SIProps = { From 81469144e8229b3302f12c35e7f322ab1326d42a Mon Sep 17 00:00:00 2001 From: chrisnojima Date: Fri, 27 Mar 2026 15:35:02 -0400 Subject: [PATCH 04/14] WIP --- shared/profile/post-proof.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/shared/profile/post-proof.tsx b/shared/profile/post-proof.tsx index 85c09661bc8f..da0428c1b980 100644 --- a/shared/profile/post-proof.tsx +++ b/shared/profile/post-proof.tsx @@ -19,8 +19,13 @@ const Container = ({error, platform, username, proofText: initialProofText}: Pro let proofText = initialProofText const cancelAddProof = useProfileState(s => s.dispatch.dynamic.cancelAddProof) const afterCheckProof = useProfileState(s => s.dispatch.dynamic.afterCheckProof) - if (platform === 'zcash' || platform === 'btc' || platform === 'dnsOrGenericWebSite' || platform === 'pgp') { - throw new Error(`Invalid profile platform in PostProofContainer: ${platform || ''}`) + if ( + platform === 'zcash' || + platform === 'btc' || + platform === 'dnsOrGenericWebSite' || + platform === 'pgp' + ) { + throw new Error(`Invalid profile platform in PostProofContainer: ${platform}`) } let url = '' From 1076e24f5f6126c1a85bcd4b24b9ac6e8a1cfe3c Mon Sep 17 00:00:00 2001 From: Chris Nojima Date: Fri, 27 Mar 2026 15:44:29 -0400 Subject: [PATCH 05/14] WIP --- shared/chat/blocking/invitation-to-block.tsx | 5 ++--- shared/chat/conversation/header-area/index.native.tsx | 5 ++--- shared/chat/conversation/info-panel/members.tsx | 5 ++--- .../messages/message-popup/exploding-header.tsx | 5 ++--- .../chat/conversation/messages/message-popup/header.tsx | 5 ++--- .../chat/conversation/messages/message-popup/hooks.tsx | 9 ++++----- shared/chat/conversation/messages/reset-user.tsx | 5 ++--- shared/chat/conversation/messages/separator.tsx | 7 +++---- shared/chat/conversation/rekey/container.tsx | 4 ++-- shared/common-adapters/mention-container.tsx | 5 ++--- shared/common-adapters/name-with-icon.tsx | 5 ++--- shared/common-adapters/profile-card.tsx | 5 ++--- shared/common-adapters/proof-broken-banner.tsx | 5 ++--- shared/common-adapters/usernames.tsx | 4 ++-- shared/constants/deeplinks.tsx | 6 +++--- shared/constants/init/shared.tsx | 5 +++-- shared/fs/banner/reset-banner.tsx | 5 ++--- shared/people/container.tsx | 6 ++---- shared/people/todo.tsx | 5 ++--- shared/profile/confirm-or-pending.tsx | 5 ++--- shared/profile/generic/result.tsx | 5 ++--- shared/profile/revoke.tsx | 5 ++--- shared/profile/user/friend.tsx | 6 ++---- shared/router-v2/account-switcher/index.tsx | 5 ++--- shared/router-v2/tab-bar.desktop.tsx | 5 ++--- shared/stores/profile.tsx | 6 +----- shared/teams/channel/rows.tsx | 5 ++--- shared/teams/external-team.tsx | 5 ++--- shared/teams/team/member/index.new.tsx | 5 ++--- shared/teams/team/rows/bot-row/bot.tsx | 5 ++--- shared/teams/team/rows/invite-row/request.tsx | 5 ++--- shared/teams/team/rows/member-row.tsx | 7 +++---- shared/tracker/assertion.tsx | 4 ++-- 33 files changed, 71 insertions(+), 103 deletions(-) diff --git a/shared/chat/blocking/invitation-to-block.tsx b/shared/chat/blocking/invitation-to-block.tsx index 7b5c9236c956..b0bffe4097dd 100644 --- a/shared/chat/blocking/invitation-to-block.tsx +++ b/shared/chat/blocking/invitation-to-block.tsx @@ -1,8 +1,8 @@ import * as Chat from '@/stores/chat' -import {useProfileState} from '@/stores/profile' import * as Kb from '@/common-adapters' import {useSafeNavigation} from '@/util/safe-navigation' import {useCurrentUserState} from '@/stores/current-user' +import {navToProfile} from '@/constants/router' const BlockButtons = () => { const nav = useSafeNavigation() @@ -16,7 +16,6 @@ const BlockButtons = () => { }) const participantInfo = Chat.useChatContext(s => s.participants) const currentUser = useCurrentUserState(s => s.username) - const showUserProfile = useProfileState(s => s.dispatch.showUserProfile) const dismissBlockButtons = Chat.useChatContext(s => s.dispatch.dismissBlockButtons) if (!blockButtonInfo) { return null @@ -26,7 +25,7 @@ const BlockButtons = () => { person => person !== currentUser && person !== adder && !Chat.isAssertion(person) ) - const onViewProfile = () => showUserProfile(adder) + const onViewProfile = () => navToProfile(adder) const onViewTeam = () => nav.safeNavigateAppend({name: 'team', params: {teamID}}) const onBlock = () => nav.safeNavigateAppend({ diff --git a/shared/chat/conversation/header-area/index.native.tsx b/shared/chat/conversation/header-area/index.native.tsx index c1ab4b8031c1..8b789d79f511 100644 --- a/shared/chat/conversation/header-area/index.native.tsx +++ b/shared/chat/conversation/header-area/index.native.tsx @@ -1,6 +1,5 @@ import * as C from '@/constants' import * as Chat from '@/stores/chat' -import {useProfileState} from '@/stores/profile' import * as Kb from '@/common-adapters' import type {HeaderBackButtonProps} from '@react-navigation/elements' import {HeaderLeftButton} from '@/common-adapters/header-buttons' @@ -10,6 +9,7 @@ import {assertionToDisplay} from '@/common-adapters/usernames' import {useSafeAreaFrame} from 'react-native-safe-area-context' import {useUsersState} from '@/stores/users' import {useCurrentUserState} from '@/stores/current-user' +import {navToProfile} from '@/constants/router' export const HeaderAreaRight = () => { const conversationIDKey = Chat.useChatContext(s => s.id) @@ -221,9 +221,8 @@ const UsernameHeader = () => { return {participants, theirFullname} }) ) - const showUserProfile = useProfileState(s => s.dispatch.showUserProfile) const onShowProfile = (username: string) => { - showUserProfile(username) + navToProfile(username) } const maxWidthStyle = useMaxWidthStyle() diff --git a/shared/chat/conversation/info-panel/members.tsx b/shared/chat/conversation/info-panel/members.tsx index f838a6cd6f93..946dec8a5491 100644 --- a/shared/chat/conversation/info-panel/members.tsx +++ b/shared/chat/conversation/info-panel/members.tsx @@ -1,12 +1,12 @@ import * as C from '@/constants' import * as Chat from '@/stores/chat' -import {useProfileState} from '@/stores/profile' import * as Teams from '@/stores/teams' import * as React from 'react' import * as Kb from '@/common-adapters' import * as T from '@/constants/types' import Participant from './participant' import {useUsersState} from '@/stores/users' +import {navToProfile} from '@/constants/router' type Props = { commonSections: ReadonlyArray

@@ -88,8 +88,7 @@ const MembersTab = (props: Props) => { return l.username.localeCompare(r.username) }) - const showUserProfile = useProfileState(s => s.dispatch.showUserProfile) - const onShowProfile = showUserProfile + const onShowProfile = navToProfile const participantSection: Section = { data: showSpinner diff --git a/shared/chat/conversation/messages/message-popup/exploding-header.tsx b/shared/chat/conversation/messages/message-popup/exploding-header.tsx index e5e5ee74d769..d14c28d6e823 100644 --- a/shared/chat/conversation/messages/message-popup/exploding-header.tsx +++ b/shared/chat/conversation/messages/message-popup/exploding-header.tsx @@ -1,8 +1,8 @@ import * as React from 'react' -import {useProfileState} from '@/stores/profile' import * as Kb from '@/common-adapters' import {formatTimeForPopup, formatTimeForRevoked, msToDHMS} from '@/util/timestamp' import {addTicker, removeTicker} from '@/util/second-timer' +import {navToProfile} from '@/constants/router' type Props = { explodesAt: number @@ -36,9 +36,8 @@ const ExplodingPopupHeader = (props: Props) => { } }, [explodesAt]) - const showUserProfile = useProfileState(s => s.dispatch.showUserProfile) const onUsernameClicked = (user: string) => { - showUserProfile(user) + navToProfile(user) onHidden() } diff --git a/shared/chat/conversation/messages/message-popup/header.tsx b/shared/chat/conversation/messages/message-popup/header.tsx index 96633abe52d1..33c3a8d821e4 100644 --- a/shared/chat/conversation/messages/message-popup/header.tsx +++ b/shared/chat/conversation/messages/message-popup/header.tsx @@ -1,7 +1,7 @@ import * as Kb from '@/common-adapters' -import {useProfileState} from '@/stores/profile' import {formatTimeForPopup, formatTimeForRevoked} from '@/util/timestamp' import type * as T from '@/constants/types' +import {navToProfile} from '@/constants/router' const iconNameForDeviceType = Kb.Styles.isMobile ? (deviceType: string, isRevoked: boolean, isLocation: boolean): Kb.IconType => { @@ -53,9 +53,8 @@ const MessagePopupHeader = (props: Props) => { const iconName = iconNameForDeviceType(deviceType, !!deviceRevokedAt, isLocation) const whoRevoked = yourMessage ? 'You' : author - const showUserProfile = useProfileState(s => s.dispatch.showUserProfile) const onUsernameClicked = (user: string) => { - showUserProfile(user) + navToProfile(user) onHidden() } diff --git a/shared/chat/conversation/messages/message-popup/hooks.tsx b/shared/chat/conversation/messages/message-popup/hooks.tsx index 158ef2fecf8a..57a53030e9da 100644 --- a/shared/chat/conversation/messages/message-popup/hooks.tsx +++ b/shared/chat/conversation/messages/message-popup/hooks.tsx @@ -3,13 +3,13 @@ import * as C from '@/constants' import * as Chat from '@/stores/chat' import * as Teams from '@/stores/teams' import {useConfigState} from '@/stores/config' -import {useProfileState} from '@/stores/profile' import {useCurrentUserState} from '@/stores/current-user' import {linkFromConvAndMessage} from '@/constants/deeplinks' import ReactionItem from './reactionitem' import MessagePopupHeader from './header' import ExplodingPopupHeader from './exploding-header' import {formatTimeForPopup, formatTimeForRevoked} from '@/util/timestamp' +import {navToProfile} from '@/constants/router' const emptyText = Chat.makeMessageText({}) @@ -232,11 +232,10 @@ export const useItems = (ordinal: T.Chat.Ordinal, onHidden: () => void) => { ] as const) : [] - const _showUserProfile = useProfileState(s => s.dispatch.showUserProfile) - const showUserProfile = () => { - _showUserProfile(author) + const onShowProfile = () => { + navToProfile(author) } - const onViewProfile = author && !yourMessage ? showUserProfile : undefined + const onViewProfile = author && !yourMessage ? onShowProfile : undefined const profileSubtitle = `${deviceName} ${deviceRevokedAt ? 'REVOKED at' : '-'} ${ deviceRevokedAt ? `${formatTimeForRevoked(deviceRevokedAt)}` : formatTimeForPopup(timestamp) }` diff --git a/shared/chat/conversation/messages/reset-user.tsx b/shared/chat/conversation/messages/reset-user.tsx index 3e34471286ed..315d02340302 100644 --- a/shared/chat/conversation/messages/reset-user.tsx +++ b/shared/chat/conversation/messages/reset-user.tsx @@ -1,6 +1,6 @@ import * as Chat from '@/stores/chat' -import {useProfileState} from '@/stores/profile' import * as Kb from '@/common-adapters' +import {navToProfile} from '@/constants/router' const ResetUser = () => { const meta = Chat.useChatContext(s => s.meta) @@ -9,8 +9,7 @@ const ResetUser = () => { const resetLetThemIn = Chat.useChatContext(s => s.dispatch.resetLetThemIn) const _participants = participantInfo.all const _resetParticipants = meta.resetParticipants - const showUserProfile = useProfileState(s => s.dispatch.showUserProfile) - const _viewProfile = showUserProfile + const _viewProfile = navToProfile const username = [..._resetParticipants][0] || '' const nonResetUsers = new Set(_participants) _resetParticipants.forEach(r => nonResetUsers.delete(r)) diff --git a/shared/chat/conversation/messages/separator.tsx b/shared/chat/conversation/messages/separator.tsx index 03cc5ff09d90..c41ee092e677 100644 --- a/shared/chat/conversation/messages/separator.tsx +++ b/shared/chat/conversation/messages/separator.tsx @@ -7,7 +7,7 @@ import * as T from '@/constants/types' import {formatTimeForConversationList, formatTimeForChat} from '@/util/timestamp' import {OrangeLineContext} from '../orange-line-context' import {useTrackerState} from '@/stores/tracker' -import {useProfileState} from '@/stores/profile' +import {navToProfile} from '@/constants/router' const missingMessage = Chat.makeMessageDeleted({}) @@ -86,18 +86,17 @@ type AuthorProps = { showUsername: string } -// Separate component so useTeamsState/useProfileState/useTrackerState only +// Separate component so useTeamsState/useTrackerState only // subscribe when there's actually an author to show. function AuthorSection(p: AuthorProps) { const {author, botAlias, isAdhocBot, teamID, teamType, teamname, timestamp, showUsername} = p const authorRoleInTeam = useTeamsState(s => s.teamIDToMembers.get(teamID)?.get(author)?.type) - const showUserProfile = useProfileState(s => s.dispatch.showUserProfile) const showUser = useTrackerState(s => s.dispatch.showUser) const onAuthorClick = () => { if (C.isMobile) { - showUserProfile(showUsername) + navToProfile(showUsername) } else { showUser(showUsername, true) } diff --git a/shared/chat/conversation/rekey/container.tsx b/shared/chat/conversation/rekey/container.tsx index 7340687e5322..682b3b895b8f 100644 --- a/shared/chat/conversation/rekey/container.tsx +++ b/shared/chat/conversation/rekey/container.tsx @@ -1,10 +1,10 @@ import * as C from '@/constants' import * as Chat from '@/stores/chat' -import {useProfileState} from '@/stores/profile' import {useCurrentUserState} from '@/stores/current-user' import * as T from '@/constants/types' import ParticipantRekey from './participant-rekey' import YouRekey from './you-rekey' +import {navToProfile} from '@/constants/router' const Container = () => { const _you = useCurrentUserState(s => s.username) @@ -23,7 +23,7 @@ const Container = () => { ) } - const onShowProfile = useProfileState(s => s.dispatch.showUserProfile) + const onShowProfile = navToProfile return rekeyers.has(_you) ? ( diff --git a/shared/common-adapters/mention-container.tsx b/shared/common-adapters/mention-container.tsx index 3ee0e41e5709..ba3fdc19b0de 100644 --- a/shared/common-adapters/mention-container.tsx +++ b/shared/common-adapters/mention-container.tsx @@ -2,9 +2,9 @@ import * as C from '@/constants' import * as Chat from '@/stores/chat' import Mention, {type OwnProps} from './mention' import {useTrackerState} from '@/stores/tracker' -import {useProfileState} from '@/stores/profile' import {useFollowerState} from '@/stores/followers' import {useCurrentUserState} from '@/stores/current-user' +import {navToProfile} from '@/constants/router' const Container = (ownProps: OwnProps) => { let {username} = ownProps @@ -24,11 +24,10 @@ const Container = (ownProps: OwnProps) => { } })() - const showUserProfile = useProfileState(s => s.dispatch.showUserProfile) const showUser = useTrackerState(s => s.dispatch.showUser) const _onClick = () => { if (C.isMobile) { - showUserProfile(username) + navToProfile(username) } else { showUser(username, true) } diff --git a/shared/common-adapters/name-with-icon.tsx b/shared/common-adapters/name-with-icon.tsx index 45b61ae81cf4..7566a6c99924 100644 --- a/shared/common-adapters/name-with-icon.tsx +++ b/shared/common-adapters/name-with-icon.tsx @@ -13,8 +13,8 @@ import Text from './text' import type {TextType, StylesTextCrossPlatform, AllowedColors, TextTypeBold} from './text.shared' import ConnectedUsernames from './usernames' import {useTrackerState} from '@/stores/tracker' -import {useProfileState} from '@/stores/profile' import {useFollowerState} from '@/stores/followers' +import {navToProfile} from '@/constants/router' type AvatarSize = 128 | 96 | 64 | 48 | 32 | 24 | 16 @@ -430,9 +430,8 @@ const ConnectedNameWithIcon = (p: OwnProps) => { const onOpenTracker = () => { username && showUser(username, true) } - const showUserProfile = useProfileState(s => s.dispatch.showUserProfile) const onOpenUserProfile = () => { - username && showUserProfile(username) + username && navToProfile(username) } let functionOnClick: NameWithIconProps['onClick'] diff --git a/shared/common-adapters/profile-card.tsx b/shared/common-adapters/profile-card.tsx index 82458f3b6cf1..559ff605e32e 100644 --- a/shared/common-adapters/profile-card.tsx +++ b/shared/common-adapters/profile-card.tsx @@ -12,7 +12,6 @@ import {_setWithProfileCardPopup} from './usernames' import FloatingMenu from './floating-menu' import Icon from './icon' import Meta from './meta' -import {useProfileState} from '@/stores/profile' import {useFollowerState} from '@/stores/followers' import {useCurrentUserState} from '@/stores/current-user' import ProgressIndicator from './progress-indicator' @@ -23,6 +22,7 @@ import {type default as FollowButtonType} from '../profile/user/actions/follow-b import type ChatButtonType from '../chat/chat-button' import {useTrackerState} from '@/stores/tracker' import type {MeasureRef} from './measure-ref' +import {navToProfile} from '@/constants/router' const positionFallbacks = ['top center', 'bottom center'] as const @@ -178,9 +178,8 @@ const ProfileCard = ({ const changeFollow = useTrackerState(s => s.dispatch.changeFollow) const _changeFollow = (follow: boolean) => changeFollow(userDetails.guiID, follow) - const showUserProfile = useProfileState(s => s.dispatch.showUserProfile) const openProfile = () => { - showUserProfile(username) + navToProfile(username) onHide?.() } diff --git a/shared/common-adapters/proof-broken-banner.tsx b/shared/common-adapters/proof-broken-banner.tsx index f85b34efd6e5..8f94cdae7358 100644 --- a/shared/common-adapters/proof-broken-banner.tsx +++ b/shared/common-adapters/proof-broken-banner.tsx @@ -1,18 +1,17 @@ import * as C from '@/constants' import {Banner, BannerParagraph} from './banner' import {useTrackerState} from '@/stores/tracker' -import {useProfileState} from '@/stores/profile' +import {navToProfile} from '@/constants/router' const Kb = {Banner} type Props = {users?: Array} type ProofBrokenBannerNonEmptyProps = {users: Array} const ProofBrokenBannerNonEmpty = (props: ProofBrokenBannerNonEmptyProps) => { - const showUserProfile = useProfileState(s => s.dispatch.showUserProfile) const showUser = useTrackerState(s => s.dispatch.showUser) const onClickUsername = (username: string) => { if (C.isMobile) { - showUserProfile(username) + navToProfile(username) } else { showUser(username, true) } diff --git a/shared/common-adapters/usernames.tsx b/shared/common-adapters/usernames.tsx index 7467460fe405..2ed26104db17 100644 --- a/shared/common-adapters/usernames.tsx +++ b/shared/common-adapters/usernames.tsx @@ -6,9 +6,9 @@ import type {TextType, Background, StylesTextCrossPlatform, AllowedColors, LineC import type {e164ToDisplay as e164ToDisplayType} from '@/util/phone-numbers' import {useTrackerState} from '@/stores/tracker' import {useUsersState} from '@/stores/users' -import {useProfileState} from '@/stores/profile' import {useFollowerState} from '@/stores/followers' import {useCurrentUserState} from '@/stores/current-user' +import {navToProfile} from '@/constants/router' export type User = { username: string @@ -99,7 +99,7 @@ function Username(p: UsernameProps) { } else if (onUsernameClicked === 'profile') { onClicked = (evt?: React.BaseSyntheticEvent) => { evt?.stopPropagation() - useProfileState.getState().dispatch.showUserProfile(username) + navToProfile(username) } } else if (typeof onUsernameClicked === 'function') { onClicked = () => onUsernameClicked(username) diff --git a/shared/constants/deeplinks.tsx b/shared/constants/deeplinks.tsx index 79a383ada879..d1b4d1eb96f6 100644 --- a/shared/constants/deeplinks.tsx +++ b/shared/constants/deeplinks.tsx @@ -1,6 +1,6 @@ import logger from '@/logger' import * as T from '@/constants/types' -import {navigateAppend, navToThread, switchTab} from './router' +import {navigateAppend, navToProfile, navToThread, switchTab} from './router' import * as Tabs from './tabs' import {useChatState} from '@/stores/chat' import {useProfileState} from '@/stores/profile' @@ -69,11 +69,11 @@ const handleKeybaseLink = (link: string) => { case 'profile': if (parts[1] === 'show' && parts[2]) { switchTab(Tabs.peopleTab) - useProfileState.getState().dispatch.showUserProfile(parts[2]) + navToProfile(parts[2]) return } if (parts[1] === 'new-proof' && (parts.length === 3 || parts.length === 4)) { - parts.length === 4 && parts[3] && useProfileState.getState().dispatch.showUserProfile(parts[3]) + parts.length === 4 && parts[3] && navToProfile(parts[3]) useProfileState.getState().dispatch.addProof(parts[2]!, 'appLink') return } diff --git a/shared/constants/init/shared.tsx b/shared/constants/init/shared.tsx index 59861c130905..ac08929b0d9f 100644 --- a/shared/constants/init/shared.tsx +++ b/shared/constants/init/shared.tsx @@ -3,6 +3,7 @@ import * as T from '../types' import isEqual from 'lodash/isEqual' import logger from '@/logger' import * as Tabs from '@/constants/tabs' +import {navToProfile} from '@/constants/router' declare global { var __hmr_startupOnce: boolean | undefined @@ -138,7 +139,7 @@ export const initTeamBuildingCallbacks = () => { return useSettingsContactsState.getState().userCountryCode }, onShowUserProfile: (username: string) => { - useProfileState.getState().dispatch.showUserProfile(username) + navToProfile(username) }, onUsersGetBlockState: (usernames: ReadonlyArray) => { useUsersState.getState().dispatch.getBlockState(usernames) @@ -379,7 +380,7 @@ export const initTracker2Callbacks = () => { defer: { ...currentState.dispatch.defer, onShowUserProfile: (username: string) => { - useProfileState.getState().dispatch.showUserProfile(username) + navToProfile(username) }, onUsersUpdates: (updates: ReadonlyArray<{name: string; info: Partial}>) => { useUsersState.getState().dispatch.updates(updates) diff --git a/shared/fs/banner/reset-banner.tsx b/shared/fs/banner/reset-banner.tsx index a488c0c58b57..f0939d5dcfb7 100644 --- a/shared/fs/banner/reset-banner.tsx +++ b/shared/fs/banner/reset-banner.tsx @@ -6,7 +6,7 @@ import * as RowTypes from '@/fs/browser/rows/types' import {useTrackerState} from '@/stores/tracker' import {useFSState} from '@/stores/fs' import * as FS from '@/stores/fs' -import {useProfileState} from '@/stores/profile' +import {navToProfile} from '@/constants/router' type OwnProps = {path: T.FS.Path} @@ -24,11 +24,10 @@ const ConnectedBanner = (ownProps: OwnProps) => { const _onReAddToTeam = (id: T.RPCGen.TeamID, username: string) => { letResetUserBackIn(id, username) } - const showUserProfile = useProfileState(s => s.dispatch.showUserProfile) const showUser = useTrackerState(s => s.dispatch.showUser) const onViewProfile = (username: string) => () => { - C.isMobile ? showUserProfile(username) : showUser(username, true) + C.isMobile ? navToProfile(username) : showUser(username, true) } const onOpenWithoutResetUsers = () => _onOpenWithoutResetUsers( diff --git a/shared/people/container.tsx b/shared/people/container.tsx index 1861d50b75c4..e56f23293068 100644 --- a/shared/people/container.tsx +++ b/shared/people/container.tsx @@ -12,10 +12,10 @@ import People from '.' import * as T from '@/constants/types' import {useFollowerState} from '@/stores/followers' import {useSignupState} from '@/stores/signup' -import {useProfileState} from '@/stores/profile' import {usePeopleState} from '@/stores/people' import {useCurrentUserState} from '@/stores/current-user' import type {e164ToDisplay as e164ToDisplayType} from '@/util/phone-numbers' +import {navToProfile} from '@/constants/router' const getPeopleDataWaitingKey = 'getPeopleData' const waitToRefresh = 1000 * 60 * 5 @@ -426,9 +426,7 @@ const PeopleReloadable = () => { } }, []) - const showUserProfile = useProfileState(s => s.dispatch.showUserProfile) - - const onClickUser = (username: string) => showUserProfile(username) + const onClickUser = (username: string) => navToProfile(username) const onReload = (isRetry?: boolean) => getData(false, isRetry === true || !followSuggestions.length) diff --git a/shared/people/todo.tsx b/shared/people/todo.tsx index 822416a819c0..0595af10db55 100644 --- a/shared/people/todo.tsx +++ b/shared/people/todo.tsx @@ -9,8 +9,8 @@ import * as Kb from '@/common-adapters' import {useSettingsEmailState} from '@/stores/settings-email' import {settingsAccountTab, settingsGitTab} from '@/constants/settings' import {useTrackerState} from '@/stores/tracker' -import {useProfileState} from '@/stores/profile' import {useCurrentUserState} from '@/stores/current-user' +import {navToProfile} from '@/constants/router' const todoTypes: {[K in T.People.TodoType]: T.People.TodoType} = { addEmail: 'addEmail', @@ -133,8 +133,7 @@ const BioConnector = (props: TodoOwnProps) => { const ProofConnector = (props: TodoOwnProps) => { const myUsername = useCurrentUserState(s => s.username) - const showUserProfile = useProfileState(s => s.dispatch.showUserProfile) - const onConfirm = showUserProfile + const onConfirm = navToProfile const onDismiss = useOnSkipTodo(props.skipTodo, 'proof') const buttons = makeDefaultButtons(() => onConfirm(myUsername), props.confirmLabel, onDismiss) return diff --git a/shared/profile/confirm-or-pending.tsx b/shared/profile/confirm-or-pending.tsx index b32f629ab4bf..2b609f95a91c 100644 --- a/shared/profile/confirm-or-pending.tsx +++ b/shared/profile/confirm-or-pending.tsx @@ -1,10 +1,10 @@ -import {useProfileState} from '@/stores/profile' import * as T from '@/constants/types' import * as Kb from '@/common-adapters' import {subtitle} from '@/util/platforms' import Modal from './modal' import {useCurrentUserState} from '@/stores/current-user' import * as C from '@/constants' +import {navToProfile} from '@/constants/router' type Props = { platform: T.More.PlatformsExpandedType @@ -14,7 +14,6 @@ type Props = { } const ConfirmOrPending = ({platform, proofFound, proofStatus, username}: Props) => { - const showUserProfile = useProfileState(s => s.dispatch.showUserProfile) const currentUsername = useCurrentUserState(s => s.username) const clearModals = C.useRouterState(s => s.dispatch.clearModals) @@ -25,7 +24,7 @@ const ConfirmOrPending = ({platform, proofFound, proofStatus, username}: Props) const platformIconOverlayColor = isGood ? Kb.Styles.globalColors.green : Kb.Styles.globalColors.greyDark const onCancel = () => { clearModals() - showUserProfile(currentUsername) + navToProfile(currentUsername) } const message = diff --git a/shared/profile/generic/result.tsx b/shared/profile/generic/result.tsx index b0fc47bdb035..c239da3bedb3 100644 --- a/shared/profile/generic/result.tsx +++ b/shared/profile/generic/result.tsx @@ -1,9 +1,9 @@ import * as C from '@/constants' -import {useProfileState} from '@/stores/profile' import * as Kb from '@/common-adapters' import {SiteIcon} from './shared' import {useCurrentUserState} from '@/stores/current-user' import type {ProveGenericParams} from '@/stores/profile' +import {navToProfile} from '@/constants/router' type Props = { error?: string @@ -14,12 +14,11 @@ type Props = { const GenericResult = ({error = '', genericParams, username}: Props) => { const proofUsername = username + genericParams.suffix const serviceIcon = genericParams.logoFull - const showUserProfile = useProfileState(s => s.dispatch.showUserProfile) const currentUsername = useCurrentUserState(s => s.username) const clearModals = C.useRouterState(s => s.dispatch.clearModals) const onClose = () => { clearModals() - showUserProfile(currentUsername) + navToProfile(currentUsername) } const success = !error diff --git a/shared/profile/revoke.tsx b/shared/profile/revoke.tsx index e6d326b7cd4c..641dd17a44dd 100644 --- a/shared/profile/revoke.tsx +++ b/shared/profile/revoke.tsx @@ -1,5 +1,4 @@ import * as C from '@/constants' -import {useProfileState} from '@/stores/profile' import * as Kb from '@/common-adapters' import * as React from 'react' import capitalize from 'lodash/capitalize' @@ -10,6 +9,7 @@ import Modal from './modal' import {useCurrentUserState} from '@/stores/current-user' import {useTrackerState} from '@/stores/tracker' import {generateGUIID} from '@/constants/utils' +import {navToProfile} from '@/constants/router' type OwnProps = { icon: T.Tracker.SiteIconSet @@ -23,13 +23,12 @@ const RevokeProof = (ownProps: OwnProps) => { const currentUsername = useCurrentUserState(s => s.username) const assertions = useTrackerState(s => s.getDetails(currentUsername).assertions) const loadProfile = useTrackerState(s => s.dispatch.load) - const showUserProfile = useProfileState(s => s.dispatch.showUserProfile) const revokeKey = C.useRPC(T.RPCGen.revokeRevokeKeyRpcPromise) const revokeSigs = C.useRPC(T.RPCGen.revokeRevokeSigsRpcPromise) const clearModals = C.useRouterState(s => s.dispatch.clearModals) const proof = assertions ? [...assertions.values()].find(a => a.sigID === proofId) : undefined const onSuccess = () => { - showUserProfile(currentUsername) + navToProfile(currentUsername) loadProfile({assertion: currentUsername, guiID: generateGUIID(), inTracker: false, reason: ''}) clearModals() } diff --git a/shared/profile/user/friend.tsx b/shared/profile/user/friend.tsx index f8e386ae6faa..cea96345c6f3 100644 --- a/shared/profile/user/friend.tsx +++ b/shared/profile/user/friend.tsx @@ -1,7 +1,7 @@ -import {useProfileState} from '@/stores/profile' import * as Kb from '@/common-adapters' import {useUsersState} from '@/stores/users' import {useFollowerState} from '@/stores/followers' +import {navToProfile} from '@/constants/router' type OwnProps = { username: string @@ -15,10 +15,8 @@ const followSizeToStyle = { const Container = (ownProps: OwnProps) => { const {username: _username, width} = ownProps const _fullname = useUsersState(s => s.infoMap.get(ownProps.username)?.fullname ?? '') - const showUserProfile = useProfileState(s => s.dispatch.showUserProfile) - const _onClick = showUserProfile const fullname = _fullname || '' - const onClick = () => _onClick(username) + const onClick = () => navToProfile(username) const username = _username const following = useFollowerState(s => (username ? s.following.has(username) : false)) const followsYou = useFollowerState(s => (username ? s.followers.has(username) : false)) diff --git a/shared/router-v2/account-switcher/index.tsx b/shared/router-v2/account-switcher/index.tsx index 0860a79530f6..96d7605b9a22 100644 --- a/shared/router-v2/account-switcher/index.tsx +++ b/shared/router-v2/account-switcher/index.tsx @@ -6,10 +6,10 @@ import * as React from 'react' import type * as T from '@/constants/types' import {settingsLogOutTab} from '@/constants/settings' import {useTrackerState} from '@/stores/tracker' -import {useProfileState} from '@/stores/profile' import {useUsersState} from '@/stores/users' import {useCurrentUserState} from '@/stores/current-user' import {useProvisionState} from '@/stores/provision' +import {navToProfile} from '@/constants/router' const prepareAccountRows = ( accountRows: ReadonlyArray, @@ -22,7 +22,6 @@ const Container = () => { const you = useCurrentUserState(s => s.username) const fullname = useTrackerState(s => s.getDetails(you).fullname ?? '') const waiting = C.Waiting.useAnyWaiting(C.waitingKeyConfigLogin) - const _onProfileClick = useProfileState(s => s.dispatch.showUserProfile) const onLoginAsAnotherUser = useProvisionState(s => s.dispatch.startProvision) const navigateUp = C.useRouterState(s => s.dispatch.navigateUp) const onCancel = () => { @@ -50,7 +49,7 @@ const Container = () => { fullname, onCancel, onLoginAsAnotherUser, - onProfileClick: () => _onProfileClick(you), + onProfileClick: () => navToProfile(you), onSelectAccount: (username: string) => { const rows = accountRows.filter(account => account.username === username) const loggedIn = (rows.length && rows[0]?.hasStoredSecret) ?? false diff --git a/shared/router-v2/tab-bar.desktop.tsx b/shared/router-v2/tab-bar.desktop.tsx index 0ab590004c60..3440dac277f0 100644 --- a/shared/router-v2/tab-bar.desktop.tsx +++ b/shared/router-v2/tab-bar.desktop.tsx @@ -16,10 +16,10 @@ import './tab-bar.css' import {settingsLogOutTab} from '@/constants/settings' import {useTrackerState} from '@/stores/tracker' import {useFSState} from '@/stores/fs' -import {useProfileState} from '@/stores/profile' import {useNotifState} from '@/stores/notifications' import {useCurrentUserState} from '@/stores/current-user' import {useProvisionState} from '@/stores/provision' +import {navToProfile} from '@/constants/router' const {hideWindow, ctlQuit} = KB2.functions @@ -43,7 +43,6 @@ const stop = () => { const Header = () => { const username = useCurrentUserState(s => s.username) const fullname = useTrackerState(s => s.getDetails(username).fullname ?? '') - const showUserProfile = useProfileState(s => s.dispatch.showUserProfile) const startProvision = useProvisionState(s => s.dispatch.startProvision) @@ -92,7 +91,7 @@ const Header = () => { const onClickWrapper = () => { hidePopup() - showUserProfile(username) + navToProfile(username) } const menuHeader = ( diff --git a/shared/stores/profile.tsx b/shared/stores/profile.tsx index 9249aa146d15..12d57598048d 100644 --- a/shared/stores/profile.tsx +++ b/shared/stores/profile.tsx @@ -5,7 +5,7 @@ import * as Z from '@/util/zustand' import logger from '@/logger' import {openURL} from '@/util/misc' import {RPCError} from '@/util/errors' -import {navToProfile, navigateAppend} from '@/constants/router' +import {navigateAppend} from '@/constants/router' import {useCurrentUserState} from '@/stores/current-user' import type {useTrackerState} from '@/stores/tracker' import {normalizeProofUsername} from '@/profile/proof-utils' @@ -71,7 +71,6 @@ export type State = Store & { editAvatar: () => void generatePgp: (args: GeneratePgpArgs) => void resetState: () => void - showUserProfile: (username: string) => void } } @@ -470,9 +469,6 @@ export const useProfileState = Z.createZustand('profile', (set, get) => { }, })) }, - showUserProfile: username => { - navToProfile(username) - }, } return { diff --git a/shared/teams/channel/rows.tsx b/shared/teams/channel/rows.tsx index dddc68a1f76e..1f22e61f3744 100644 --- a/shared/teams/channel/rows.tsx +++ b/shared/teams/channel/rows.tsx @@ -1,12 +1,12 @@ import * as C from '@/constants' import * as Chat from '@/stores/chat' -import {useProfileState} from '@/stores/profile' import * as Teams from '@/stores/teams' import type * as T from '@/constants/types' import * as Kb from '@/common-adapters' import MenuHeader from '../team/rows/menu-header.new' import {useUsersState} from '@/stores/users' import {useCurrentUserState} from '@/stores/current-user' +import {navToProfile} from '@/constants/router' type Props = { conversationIDKey: T.Chat.ConversationIDKey @@ -127,10 +127,9 @@ const ChannelMemberRow = (props: Props) => { ) - const showUserProfile = useProfileState(s => s.dispatch.showUserProfile) const makePopup = (p: Kb.Popup2Parms) => { const {attachTo, hidePopup} = p - const onOpenProfile = () => username && showUserProfile(username) + const onOpenProfile = () => username && navToProfile(username) const onRemoveFromChannel = () => navigateAppend({ name: 'teamReallyRemoveChannelMember', diff --git a/shared/teams/external-team.tsx b/shared/teams/external-team.tsx index 510170fbf172..e4ec72182a95 100644 --- a/shared/teams/external-team.tsx +++ b/shared/teams/external-team.tsx @@ -1,6 +1,5 @@ import * as C from '@/constants' import * as Chat from '@/stores/chat' -import {useProfileState} from '@/stores/profile' import * as React from 'react' import * as Kb from '@/common-adapters' import * as T from '@/constants/types' @@ -9,6 +8,7 @@ import {useTeamLinkPopup} from './common' import {pluralize} from '@/util/string' import capitalize from 'lodash/capitalize' import {useSafeNavigation} from '@/util/safe-navigation' +import {navToProfile} from '@/constants/router' type Props = {teamname: string} @@ -177,13 +177,12 @@ const Member = ({member, firstItem}: {member: T.RPCGen.TeamMemberRole; firstItem const previewConversation = Chat.useChatState(s => s.dispatch.previewConversation) const onChat = () => previewConversation({participants: [member.username], reason: 'teamMember'}) const roleString = Teams.teamRoleByEnum[member.role] - const showUserProfile = useProfileState(s => s.dispatch.showUserProfile) return ( } - onClick={() => showUserProfile(member.username)} + onClick={() => navToProfile(member.username)} body={ diff --git a/shared/teams/team/member/index.new.tsx b/shared/teams/team/member/index.new.tsx index cd2c2e189336..50a701eedc82 100644 --- a/shared/teams/team/member/index.new.tsx +++ b/shared/teams/team/member/index.new.tsx @@ -2,7 +2,6 @@ import * as C from '@/constants' import * as Chat from '@/stores/chat' import {useCurrentUserState} from '@/stores/current-user' import * as Teams from '@/stores/teams' -import {useProfileState} from '@/stores/profile' import * as Kb from '@/common-adapters' import * as T from '@/constants/types' import * as React from 'react' @@ -14,6 +13,7 @@ import {pluralize} from '@/util/string' import {useAllChannelMetas} from '@/teams/common/channel-hooks' import {useTeamDetailsSubscribe} from '@/teams/subscriber' import {useSafeNavigation} from '@/util/safe-navigation' +import {navToProfile} from '@/constants/router' type Props = { teamID: T.Teams.TeamID @@ -618,9 +618,8 @@ export const TeamMemberHeader = (props: Props) => { ) const yourUsername = useCurrentUserState(s => s.username) const previewConversation = Chat.useChatState(s => s.dispatch.previewConversation) - const showUserProfile = useProfileState(s => s.dispatch.showUserProfile) const onChat = () => previewConversation({participants: [username], reason: 'memberView'}) - const onViewProfile = () => showUserProfile(username) + const onViewProfile = () => navToProfile(username) const onViewTeam = () => nav.safeNavigateAppend({name: 'team', params: {teamID}}) const member = teamDetails?.members.get(username) diff --git a/shared/teams/team/rows/bot-row/bot.tsx b/shared/teams/team/rows/bot-row/bot.tsx index 16aa745cb4ca..ec2e0d3f570e 100644 --- a/shared/teams/team/rows/bot-row/bot.tsx +++ b/shared/teams/team/rows/bot-row/bot.tsx @@ -5,8 +5,8 @@ import * as Kb from '@/common-adapters' import type * as T from '@/constants/types' import BotMenu from './bot-menu' import {useTrackerState} from '@/stores/tracker' -import {useProfileState} from '@/stores/profile' import {useFeaturedBot} from '@/util/featured-bots' +import {navToProfile} from '@/constants/router' export type Props = { botAlias: string @@ -179,11 +179,10 @@ const Container = (ownProps: OwnProps) => { const roleType = info.type const status = info.status const username = info.username - const showUserProfile = useProfileState(s => s.dispatch.showUserProfile) const showUser = useTrackerState(s => s.dispatch.showUser) const _onShowTracker = (username: string) => { if (C.isMobile) { - showUserProfile(username) + navToProfile(username) } else { showUser(username, true) } diff --git a/shared/teams/team/rows/invite-row/request.tsx b/shared/teams/team/rows/invite-row/request.tsx index 44d8854c4786..a5fef233bea4 100644 --- a/shared/teams/team/rows/invite-row/request.tsx +++ b/shared/teams/team/rows/invite-row/request.tsx @@ -2,12 +2,12 @@ import * as React from 'react' import * as C from '@/constants' import * as Chat from '@/stores/chat' import * as Teams from '@/stores/teams' -import {useProfileState} from '@/stores/profile' import type * as T from '@/constants/types' import * as Kb from '@/common-adapters' import {FloatingRolePicker, sendNotificationFooter} from '@/teams/role-picker' import {formatTimeRelativeToNow} from '@/util/timestamp' import MenuHeader from '../menu-header.new' +import {navToProfile} from '@/constants/router' const positionFallbacks = ['left center', 'top left'] as const @@ -238,9 +238,8 @@ const Container = (ownProps: OwnProps) => { const onChat = () => { username && previewConversation({participants: [username], reason: 'teamInvite'}) } - const showUserProfile = useProfileState(s => s.dispatch.showUserProfile) const onOpenProfile = () => { - showUserProfile(username) + navToProfile(username) } const props = { _notifLabel: _notifLabel, diff --git a/shared/teams/team/rows/member-row.tsx b/shared/teams/team/rows/member-row.tsx index 0421aaf8effd..f0a04c7fedb8 100644 --- a/shared/teams/team/rows/member-row.tsx +++ b/shared/teams/team/rows/member-row.tsx @@ -6,9 +6,9 @@ import type * as T from '@/constants/types' import MenuHeader from './menu-header.new' import {useSafeNavigation} from '@/util/safe-navigation' import {useTrackerState} from '@/stores/tracker' -import {useProfileState} from '@/stores/profile' import {useUsersState} from '@/stores/users' import {useCurrentUserState} from '@/stores/current-user' +import {navToProfile} from '@/constants/router' export type Props = { firstItem: boolean @@ -316,9 +316,8 @@ const Container = (ownProps: OwnProps) => { const onClick = () => { navigateAppend({name: 'teamMember', params: {teamID, username}}) } - const showUserProfile = useProfileState(s => s.dispatch.showUserProfile) const onOpenProfile = () => { - username && showUserProfile(username) + username && navToProfile(username) } const onReAddToTeam = () => { reAddToTeam(teamID, username) @@ -329,7 +328,7 @@ const Container = (ownProps: OwnProps) => { const showUser = useTrackerState(s => s.dispatch.showUser) const onShowTracker = () => { if (C.isMobile) { - showUserProfile(username) + navToProfile(username) } else { showUser(username, true) } diff --git a/shared/tracker/assertion.tsx b/shared/tracker/assertion.tsx index b325668141ee..b18963e91549 100644 --- a/shared/tracker/assertion.tsx +++ b/shared/tracker/assertion.tsx @@ -12,6 +12,7 @@ import * as Tracker from '@/stores/tracker' import {useTrackerState} from '@/stores/tracker' import {useProfileState} from '@/stores/profile' import {generateGUIID} from '@/constants/utils' +import {navToProfile} from '@/constants/router' type OwnProps = { isSuggestion?: boolean @@ -76,7 +77,6 @@ const Container = (ownProps: OwnProps) => { const {color, metas: _metas, proofURL, sigID, siteIcon, stellarHidden, notAUser} = data const {siteIconDarkmode, siteIconFull, siteIconFullDarkmode, siteURL, state, timestamp, type, value} = data const addProof = useProfileState(s => s.dispatch.addProof) - const showUserProfile = useProfileState(s => s.dispatch.showUserProfile) const loadProfile = useTrackerState(s => s.dispatch.load) const hideStellar = C.useRPC(T.RPCGen.apiserverPostRpcPromise) const recheckProof = C.useRPC(T.RPCGen.proveCheckProofRpcPromise) @@ -94,7 +94,7 @@ const Container = (ownProps: OwnProps) => { recheckProof( [{sigID}, C.waitingKeyProfile], () => { - showUserProfile(ownProps.username) + navToProfile(ownProps.username) loadProfile({assertion: ownProps.username, guiID: generateGUIID(), inTracker: false, reason: ''}) }, () => {} From f319c86d367bc5280bfc201e5c20e5f8b9dfb178 Mon Sep 17 00:00:00 2001 From: chrisnojima Date: Fri, 27 Mar 2026 15:45:57 -0400 Subject: [PATCH 06/14] WIP --- shared/people/todo.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/shared/people/todo.tsx b/shared/people/todo.tsx index 0595af10db55..97385f988ce4 100644 --- a/shared/people/todo.tsx +++ b/shared/people/todo.tsx @@ -11,6 +11,7 @@ import {settingsAccountTab, settingsGitTab} from '@/constants/settings' import {useTrackerState} from '@/stores/tracker' import {useCurrentUserState} from '@/stores/current-user' import {navToProfile} from '@/constants/router' +import {useProfileState} from '@/stores/profile' const todoTypes: {[K in T.People.TodoType]: T.People.TodoType} = { addEmail: 'addEmail', From b1f52092d265cda6352b4809897a3a54a3a0327e Mon Sep 17 00:00:00 2001 From: Chris Nojima Date: Fri, 27 Mar 2026 16:04:26 -0400 Subject: [PATCH 07/14] WIP --- shared/profile/routes.tsx | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/shared/profile/routes.tsx b/shared/profile/routes.tsx index 4a552d448b52..c4c1d21128bd 100644 --- a/shared/profile/routes.tsx +++ b/shared/profile/routes.tsx @@ -15,6 +15,8 @@ const styles = Kb.Styles.styleSheetCreate( }) as const ) +const profileModalStyle = {width: 560} + const EditAvatarHeaderLeft = ({wizard, showBack}: {wizard?: boolean; showBack?: boolean}) => { const navigateUp = C.useRouterState(s => s.dispatch.navigateUp) if (wizard || showBack) { @@ -80,7 +82,9 @@ export const newModalRoutes = { }, } ), - profileConfirmOrPending: C.makeScreen(React.lazy(async () => import('./confirm-or-pending'))), + profileConfirmOrPending: C.makeScreen(React.lazy(async () => import('./confirm-or-pending')), { + getOptions: {modalStyle: profileModalStyle}, + }), profileEdit: C.makeScreen(React.lazy(async () => import('./edit-profile')), { getOptions: {modalStyle: {height: 450, width: 350}, title: 'Edit Profile'}, }), @@ -101,14 +105,22 @@ export const newModalRoutes = { }), profileImport: C.makeScreen(React.lazy(async () => import('./pgp/import'))), profilePgp: C.makeScreen(React.lazy(async () => import('./pgp/choice'))), - profilePostProof: C.makeScreen(React.lazy(async () => import('./post-proof'))), + profilePostProof: C.makeScreen(React.lazy(async () => import('./post-proof')), { + getOptions: {modalStyle: profileModalStyle}, + }), profileProofsList: C.makeScreen(React.lazy(async () => import('./generic/proofs-list')), { getOptions: {modalStyle: {height: 485, width: 560}, title: 'Prove your...'}, }), - profileProveEnterUsername: C.makeScreen(React.lazy(async () => import('./prove-enter-username'))), - profileProveWebsiteChoice: C.makeScreen(React.lazy(async () => import('./prove-website-choice'))), + profileProveEnterUsername: C.makeScreen(React.lazy(async () => import('./prove-enter-username')), { + getOptions: {modalStyle: profileModalStyle}, + }), + profileProveWebsiteChoice: C.makeScreen(React.lazy(async () => import('./prove-website-choice')), { + getOptions: {modalStyle: profileModalStyle}, + }), profileProvideInfo: C.makeScreen(React.lazy(async () => import('./pgp/info'))), - profileRevoke: C.makeScreen(React.lazy(async () => import('./revoke'))), + profileRevoke: C.makeScreen(React.lazy(async () => import('./revoke')), { + getOptions: {modalStyle: profileModalStyle}, + }), profileShowcaseTeamOffer: C.makeScreen(React.lazy(async () => import('./showcase-team-offer')), { getOptions: {modalStyle: {maxHeight: 600, maxWidth: 600}, title: 'Feature your teams'}, }), From 2302ac8ab3ba7432eb90fa1ec1e1ceecd722739d Mon Sep 17 00:00:00 2001 From: Chris Nojima Date: Fri, 27 Mar 2026 16:11:14 -0400 Subject: [PATCH 08/14] WIP --- shared/stores/profile.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/shared/stores/profile.tsx b/shared/stores/profile.tsx index 12d57598048d..e69aa0f9eccf 100644 --- a/shared/stores/profile.tsx +++ b/shared/stores/profile.tsx @@ -5,7 +5,7 @@ import * as Z from '@/util/zustand' import logger from '@/logger' import {openURL} from '@/util/misc' import {RPCError} from '@/util/errors' -import {navigateAppend} from '@/constants/router' +import {getVisibleScreen, navigateAppend} from '@/constants/router' import {useCurrentUserState} from '@/stores/current-user' import type {useTrackerState} from '@/stores/tracker' import {normalizeProofUsername} from '@/profile/proof-utils' @@ -93,6 +93,9 @@ export const useProfileState = Z.createZustand('profile', (set, get) => { }) } + const shouldReplacePrompt = (name: 'profileGenericEnterUsername' | 'profileProveEnterUsername') => + getVisibleScreen()?.name === name + const checkProofAndNavigate = async ( platform: T.More.PlatformsExpandedType, sigID: T.RPCGen.SigID, @@ -283,7 +286,7 @@ export const useProfileState = Z.createZustand('profile', (set, get) => { username: currentUsername || undefined, }, }, - true + shouldReplacePrompt('profileProveEnterUsername') ) } else if (genericService && parameters) { currentGenericParams = toProveGenericParams(parameters) @@ -297,7 +300,7 @@ export const useProfileState = Z.createZustand('profile', (set, get) => { username: currentUsername || undefined, }, }, - true + shouldReplacePrompt('profileGenericEnterUsername') ) } }, From 1ad2c0d9f36798951d73ea44671197be7a0c8356 Mon Sep 17 00:00:00 2001 From: Chris Nojima Date: Fri, 27 Mar 2026 16:31:18 -0400 Subject: [PATCH 09/14] WIP --- shared/stores/profile.tsx | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/shared/stores/profile.tsx b/shared/stores/profile.tsx index e69aa0f9eccf..e09d7df2d9f2 100644 --- a/shared/stores/profile.tsx +++ b/shared/stores/profile.tsx @@ -75,6 +75,16 @@ export type State = Store & { } export const useProfileState = Z.createZustand('profile', (set, get) => { + const addProofRouteNames = new Set([ + 'profileConfirmOrPending', + 'profileGenericEnterUsername', + 'profileGenericProofResult', + 'profilePostProof', + 'profileProofsList', + 'profileProveEnterUsername', + 'profileProveWebsiteChoice', + ]) + const resetProofCallbacks = () => { set(s => { s.dispatch.dynamic.afterCheckProof = undefined @@ -83,6 +93,7 @@ export const useProfileState = Z.createZustand('profile', (set, get) => { }) } const defaultCancelAddProof = () => { + addProofInProgress = false resetProofCallbacks() } @@ -96,6 +107,18 @@ export const useProfileState = Z.createZustand('profile', (set, get) => { const shouldReplacePrompt = (name: 'profileGenericEnterUsername' | 'profileProveEnterUsername') => getVisibleScreen()?.name === name + const clearStaleAddProofGuard = () => { + if (!addProofInProgress) { + return + } + const visibleScreen = getVisibleScreen()?.name + if (visibleScreen && addProofRouteNames.has(visibleScreen)) { + return + } + logger.info('clearing stale addProof guard', {visibleScreen}) + defaultCancelAddProof() + } + const checkProofAndNavigate = async ( platform: T.More.PlatformsExpandedType, sigID: T.RPCGen.SigID, @@ -149,6 +172,7 @@ export const useProfileState = Z.createZustand('profile', (set, get) => { const dispatch: State['dispatch'] = { addProof: (platform, reason) => { + clearStaleAddProofGuard() if (addProofInProgress) { logger.warn('addProof while one in progress') return @@ -323,7 +347,7 @@ export const useProfileState = Z.createZustand('profile', (set, get) => { loadAfter() if (service) { - await checkProofAndNavigate(service, sigID, currentUsername, currentProofText) + ignorePromise(checkProofAndNavigate(service, sigID, currentUsername, currentProofText)) } else { navigateAppend( { From fc9801ed95c08d0373103ef4a8054f778cdd6c3f Mon Sep 17 00:00:00 2001 From: Chris Nojima Date: Fri, 27 Mar 2026 16:48:56 -0400 Subject: [PATCH 10/14] WIP --- shared/constants/deeplinks.tsx | 3 +- shared/profile/generic/proofs-list.tsx | 1398 +++++++++++++++++++++-- shared/profile/prove-website-choice.tsx | 13 +- shared/stores/profile.tsx | 314 +---- shared/tracker/assertion.tsx | 10 +- 5 files changed, 1309 insertions(+), 429 deletions(-) diff --git a/shared/constants/deeplinks.tsx b/shared/constants/deeplinks.tsx index d1b4d1eb96f6..666183c19a68 100644 --- a/shared/constants/deeplinks.tsx +++ b/shared/constants/deeplinks.tsx @@ -3,7 +3,6 @@ import * as T from '@/constants/types' import {navigateAppend, navToProfile, navToThread, switchTab} from './router' import * as Tabs from './tabs' import {useChatState} from '@/stores/chat' -import {useProfileState} from '@/stores/profile' import {useTeamsState} from '@/stores/teams' const prefix = 'keybase://' @@ -74,7 +73,7 @@ const handleKeybaseLink = (link: string) => { } if (parts[1] === 'new-proof' && (parts.length === 3 || parts.length === 4)) { parts.length === 4 && parts[3] && navToProfile(parts[3]) - useProfileState.getState().dispatch.addProof(parts[2]!, 'appLink') + navigateAppend({name: 'profileProofsList', params: {platform: parts[2]!, reason: 'appLink'}}) return } break diff --git a/shared/profile/generic/proofs-list.tsx b/shared/profile/generic/proofs-list.tsx index a1ae3260ec5b..c4d1bb8925d4 100644 --- a/shared/profile/generic/proofs-list.tsx +++ b/shared/profile/generic/proofs-list.tsx @@ -1,149 +1,1249 @@ import * as C from '@/constants' import * as Kb from '@/common-adapters' import * as React from 'react' -import type * as T from '@/constants/types' -import {SiteIcon} from './shared' +import * as T from '@/constants/types' import {makeInsertMatcher} from '@/util/string' import {useColorScheme} from 'react-native' import {useTrackerState} from '@/stores/tracker' -import {useProfileState} from '@/stores/profile' +import Modal from '../modal' +import {SiteIcon} from './shared' +import {normalizeProofUsername} from '../proof-utils' +import {useConfigState} from '@/stores/config' +import {openURL as openUrl} from '@/util/misc' +import {subtitle} from '@/util/platforms' +import {useCurrentUserState} from '@/stores/current-user' +import {generateGUIID, ignorePromise} from '@/constants/utils' +import {RPCError} from '@/util/errors' +import logger from '@/logger' +import {navToProfile} from '@/constants/router' -const Container = () => { - const _proofSuggestions = useTrackerState(s => s.proofSuggestions) - const navigateUp = C.useRouterState(s => s.dispatch.navigateUp) - const onCancel = () => { - navigateUp() - } - const addProof = useProfileState(s => s.dispatch.addProof) - const providerClicked = (key: string) => { - addProof(key, 'profile') - } +type ProveGenericParams = { + buttonLabel: string + logoBlack: T.Tracker.SiteIconSet + logoFull: T.Tracker.SiteIconSet + subtext: string + suffix: string + title: string +} +const makeProveGenericParams = (): ProveGenericParams => ({ + buttonLabel: '', + logoBlack: [], + logoFull: [], + subtext: '', + suffix: '', + title: '', +}) + +const toProveGenericParams = (p: T.RPCGen.ProveParameters): ProveGenericParams => ({ + ...makeProveGenericParams(), + buttonLabel: p.buttonLabel, + logoBlack: p.logoBlack || [], + logoFull: p.logoFull || [], + subtext: p.subtext, + suffix: p.suffix, + title: p.title, +}) + +type Props = { + platform?: string + reason?: 'appLink' | 'profile' +} + +type Provider = { + desc: string + icon: T.Tracker.SiteIconSet + key: string + name: string + new: boolean +} + +type PickStep = {kind: 'pick'} +type LoadingStep = {kind: 'loading'} +type WebsiteChoiceStep = {kind: 'websiteChoice'} +type EnterUsernameStep = { + error: string + kind: 'enterUsername' + platform: T.More.PlatformsExpandedType + username: string +} +type GenericEnterUsernameStep = { + error: string + genericParams: ProveGenericParams + kind: 'genericEnterUsername' + proofUrl?: string + service: string + username: string +} +type GenericResultStep = { + error: string + genericParams: ProveGenericParams + kind: 'genericResult' + username: string +} +type PostProofStep = { + error: string + kind: 'postProof' + platform: T.More.PlatformsExpandedType + proofText: string + sigID?: T.RPCGen.SigID + username: string +} +type ConfirmOrPendingStep = { + kind: 'confirmOrPending' + platform: T.More.PlatformsExpandedType + proofFound: boolean + proofStatus?: T.RPCGen.ProofStatus + username: string +} +type Step = + | PickStep + | LoadingStep + | WebsiteChoiceStep + | EnterUsernameStep + | GenericEnterUsernameStep + | GenericResultStep + | PostProofStep + | ConfirmOrPendingStep + +const Container = ({platform, reason = 'profile'}: Props) => { + const currentUsername = useCurrentUserState(s => s.username) + const loadProfile = useTrackerState(s => s.dispatch.load) + const proofSuggestions = useTrackerState(s => s.proofSuggestions) + const copyToClipboard = useConfigState(s => s.dispatch.defer.copyToClipboard) + const registerCryptoAddress = C.useRPC(T.RPCGen.cryptocurrencyRegisterAddressRpcPromise) const isDarkMode = useColorScheme() === 'dark' - const providers = _proofSuggestions.map(s => ({ + const {clearModals, navigateAppend, navigateUp} = C.useRouterState( + C.useShallow(s => ({ + clearModals: s.dispatch.clearModals, + navigateAppend: s.dispatch.navigateAppend, + navigateUp: s.dispatch.navigateUp, + })) + ) + + const providers = proofSuggestions.map(s => ({ desc: s.pickerSubtext, icon: isDarkMode ? s.siteIconFullDarkmode : s.siteIconFull, key: s.assertionKey, name: s.pickerText, new: s.metas.some(({label}) => label === 'new'), })) - const title = 'Prove your...' + + const mountedRef = React.useRef(true) + const initialRouteRef = React.useRef({platform, reason}) + const currentUsernameRef = React.useRef('') + const currentGenericParamsRef = React.useRef(makeProveGenericParams()) + const afterCheckProofRef = React.useRef void)>(undefined) + const cancelCurrentRef = React.useRef void)>(undefined) + const submitUsernameRef = React.useRef void)>(undefined) + const startProofRef = React.useRef<(proofPlatform: string, proofReason: 'appLink' | 'profile') => void>(() => {}) + const [step, setStep] = React.useState(platform ? {kind: 'loading'} : {kind: 'pick'}) + + const setStepSafe = (next: Step) => { + if (mountedRef.current) { + setStep(next) + } + } + + const resetSession = () => { + afterCheckProofRef.current = undefined + cancelCurrentRef.current = undefined + submitUsernameRef.current = undefined + currentGenericParamsRef.current = makeProveGenericParams() + currentUsernameRef.current = '' + } + + const cancelSession = () => { + const cancel = cancelCurrentRef.current + resetSession() + cancel?.() + } + + React.useEffect(() => { + return () => { + mountedRef.current = false + cancelSession() + } + }, []) + + const loadCurrentProfile = () => + loadProfile({assertion: currentUsername, guiID: generateGUIID(), inTracker: false, reason: ''}) + + const closeModal = () => { + cancelSession() + navigateUp() + } + + const closeToProfile = () => { + cancelSession() + clearModals() + navToProfile(currentUsername) + loadCurrentProfile() + } + + const checkProofAndNavigate = async ( + proofPlatform: T.More.PlatformsExpandedType, + sigID: T.RPCGen.SigID, + username: string, + proofText: string + ) => { + try { + const {found, status} = await T.RPCGen.proveCheckProofRpcPromise({sigID}, C.waitingKeyProfile) + if (!mountedRef.current) return + if (!found && status >= T.RPCGen.ProofStatus.baseHardError) { + setStepSafe({ + error: "We couldn't find your proof. Please retry!", + kind: 'postProof', + platform: proofPlatform, + proofText, + sigID, + username, + }) + } else { + setStepSafe({ + kind: 'confirmOrPending', + platform: proofPlatform, + proofFound: found, + proofStatus: status, + username, + }) + } + } catch { + logger.warn('Error getting proof update') + setStepSafe({ + error: "We couldn't verify your proof. Please retry!", + kind: 'postProof', + platform: proofPlatform, + proofText, + sigID, + username, + }) + } + } + + const startProof = (proofPlatform: string, proofReason: 'appLink' | 'profile') => { + const service = T.More.asPlatformsExpandedType(proofPlatform) + const genericService = service ? null : proofPlatform + + switch (service) { + case 'dnsOrGenericWebSite': + setStepSafe({kind: 'websiteChoice'}) + return + case 'zcash': + case 'btc': + setStepSafe({error: '', kind: 'enterUsername', platform: service, username: ''}) + return + case 'pgp': + navigateAppend('profilePgp') + return + default: + break + } + + setStepSafe({kind: 'loading'}) + + const inputCancelError = { + code: T.RPCGen.StatusCode.scinputcanceled, + desc: 'Cancel Add Proof', + } + + let canceled = false + let proofText = '' + currentUsernameRef.current = '' + currentGenericParamsRef.current = makeProveGenericParams() + + const failIfCanceled = (response: {error: (arg0: {code: number; desc: string}) => void}) => { + if (canceled || !mountedRef.current) { + response.error(inputCancelError) + return true + } + return false + } + + ignorePromise( + (async () => { + try { + const {sigID} = await T.RPCGen.proveStartProofRpcListener({ + customResponseIncomingCallMap: { + 'keybase.1.proveUi.checking': (_, response) => { + if (failIfCanceled(response)) { + return + } + response.result() + }, + 'keybase.1.proveUi.continueChecking': (_, response) => + response.result(!(canceled || !mountedRef.current)), + 'keybase.1.proveUi.okToCheck': (_, response) => response.result(true), + 'keybase.1.proveUi.outputInstructions': ({proof}, response) => { + if (failIfCanceled(response)) { + return + } + afterCheckProofRef.current = () => { + afterCheckProofRef.current = undefined + response.result() + } + cancelCurrentRef.current = () => { + canceled = true + response.error(inputCancelError) + } + + if (service && proof) { + proofText = proof + setStepSafe({ + error: '', + kind: 'postProof', + platform: service, + proofText: proof, + username: currentUsernameRef.current, + }) + } else if (proof) { + const genericParams = currentGenericParamsRef.current + setStepSafe({ + error: '', + genericParams, + kind: 'genericEnterUsername', + proofUrl: proof, + service: genericService ?? '', + username: currentUsernameRef.current, + }) + openUrl(proof) + afterCheckProofRef.current?.() + } + }, + 'keybase.1.proveUi.preProofWarning': (_, response) => response.result(true), + 'keybase.1.proveUi.promptOverwrite': (_, response) => response.result(true), + 'keybase.1.proveUi.promptUsername': (args, response) => { + if (failIfCanceled(response)) { + return + } + const {parameters, prevError} = args + cancelCurrentRef.current = () => { + canceled = true + response.error(inputCancelError) + } + submitUsernameRef.current = (username: string) => { + const {normalized} = normalizeProofUsername(service, username) + currentUsernameRef.current = normalized + submitUsernameRef.current = undefined + response.result(normalized) + } + if (service) { + setStepSafe({ + error: prevError?.desc ?? '', + kind: 'enterUsername', + platform: service, + username: currentUsernameRef.current, + }) + cancelCurrentRef.current = () => { + canceled = true + response.error(inputCancelError) + } + } else if (genericService && parameters) { + currentGenericParamsRef.current = toProveGenericParams(parameters) + setStepSafe({ + error: prevError?.desc ?? '', + genericParams: currentGenericParamsRef.current, + kind: 'genericEnterUsername', + service: genericService, + username: currentUsernameRef.current, + }) + } + afterCheckProofRef.current = undefined + }, + }, + incomingCallMap: { + 'keybase.1.proveUi.displayRecheckWarning': () => {}, + 'keybase.1.proveUi.outputPrechecks': () => {}, + }, + params: { + auto: false, + force: true, + promptPosted: !!genericService, + service: proofPlatform, + username: '', + }, + waitingKey: C.waitingKeyProfile, + }) + + loadCurrentProfile() + + if (service) { + ignorePromise(checkProofAndNavigate(service, sigID, currentUsernameRef.current, proofText)) + } else { + setStepSafe({ + error: '', + genericParams: currentGenericParamsRef.current, + kind: 'genericResult', + username: currentUsernameRef.current, + }) + } + } catch (_error) { + loadCurrentProfile() + if (!(_error instanceof RPCError)) { + return + } + const error = _error + logger.warn('Error making proof') + + if (genericService) { + setStepSafe({ + error: error.desc || 'Failed to verify proof', + genericParams: currentGenericParamsRef.current, + kind: 'genericResult', + username: currentUsernameRef.current, + }) + } else if (proofReason === 'appLink' && error.code === T.RPCGen.StatusCode.scgeneric) { + navigateUp() + navigateAppend({ + name: 'keybaseLinkError', + params: { + error: + "We couldn't find a valid service for proofs in this link. The link might be bad, or your Keybase app might be out of date and need to be updated.", + }, + }) + } + } finally { + resetSession() + } + })() + ) + } + + const onSubmitProofUsername = (proofPlatform: T.More.PlatformsExpandedType, input: string) => { + const {normalized, valid} = normalizeProofUsername(proofPlatform, input) + + if (proofPlatform === 'btc') { + if (!valid) { + setStepSafe({error: 'Invalid address format', kind: 'enterUsername', platform: proofPlatform, username: input}) + return + } + registerCryptoAddress( + [{address: normalized, force: true, wantedFamily: 'bitcoin'}, C.waitingKeyProfile], + () => { + setStepSafe({ + kind: 'confirmOrPending', + platform: proofPlatform, + proofFound: true, + proofStatus: T.RPCGen.ProofStatus.ok, + username: normalized, + }) + loadCurrentProfile() + }, + error => { + setStepSafe({error: error.desc, kind: 'enterUsername', platform: proofPlatform, username: input}) + } + ) + return + } + + if (proofPlatform === 'zcash') { + registerCryptoAddress( + [{address: normalized, force: true, wantedFamily: 'zcash'}, C.waitingKeyProfile], + () => { + setStepSafe({ + kind: 'confirmOrPending', + platform: proofPlatform, + proofFound: true, + proofStatus: T.RPCGen.ProofStatus.ok, + username: normalized, + }) + loadCurrentProfile() + }, + error => { + setStepSafe({error: error.desc, kind: 'enterUsername', platform: proofPlatform, username: input}) + } + ) + return + } + + currentUsernameRef.current = normalized + if (!submitUsernameRef.current) { + return + } + submitUsernameRef.current(normalized) + } + + startProofRef.current = startProof + + React.useEffect(() => { + const {platform: initialPlatform, reason: initialReason} = initialRouteRef.current + if (initialPlatform) { + startProofRef.current(initialPlatform, initialReason) + } + }, []) + + const content = (() => { + switch (step.kind) { + case 'loading': + return ( + + + + + Starting proof... + + + + ) + case 'websiteChoice': + return ( + + + + Prove your website in two ways: + + startProof('web', 'profile'), + title: 'Host a TXT file', + }, + { + description: 'Place a Keybase proof in your DNS records.', + icon: 'icon-dns-48', + onClick: () => startProof('dns', 'profile'), + title: 'Set a DNS', + }, + ]} + /> + + + ) + case 'enterUsername': + return ( + + ) + case 'postProof': + return ( + { + if (afterCheckProofRef.current) { + const submit = afterCheckProofRef.current + afterCheckProofRef.current = undefined + submit() + return + } + step.sigID && ignorePromise(checkProofAndNavigate(step.platform, step.sigID, step.username, step.proofText)) + }} + step={step} + /> + ) + case 'confirmOrPending': + return + case 'genericEnterUsername': + return ( + { + if (step.proofUrl) { + openUrl(step.proofUrl) + return + } + currentUsernameRef.current = username + if (!submitUsernameRef.current) { + return + } + submitUsernameRef.current(username) + }} + step={step} + /> + ) + case 'genericResult': + return + case 'pick': + return startProof(key, 'profile')} providers={providers} /> + } + })() + + return <>{content} +} + +const ProviderPicker = ({ + onCancel, + onSelect, + providers, +}: { + onCancel: () => void + onSelect: (key: string) => void + providers: Array +}) => { const [filter, setFilter] = React.useState('') + const itemHeight = { + height: Kb.Styles.isMobile ? 56 : 48, + type: 'fixed', + } as const + const filterRegexp = makeInsertMatcher(filter) + const items = (() => { + const exact: Array = [] + const inexact: Array = [] + providers.forEach(p => { + if (p.name === filter) { + exact.push(p) + } else if (filterProvider(p, filterRegexp)) { + inexact.push(p) + } + }) + return [...exact, ...inexact] + })() return ( - <> - - - - - + + + + + ( + + + onSelect(provider.key)} + style={styles.containerBox} + > + + + + {provider.name} + + {(provider.new || !!provider.desc) && ( + + {provider.new && ( + + )} + + {provider.desc} + + + )} + + + + + )} /> - + + + + - + ) } -export type IdentityProvider = { - name: string - desc: string - icon: T.Tracker.SiteIconSet - key: string - new: boolean +const EnterUsername = ({ + error, + onCancel, + onSubmit, + platform, + username: initialUsername, +}: { + error: string + onCancel: () => void + onSubmit: (platform: T.More.PlatformsExpandedType, username: string) => void + platform: T.More.PlatformsExpandedType + username: string +}) => { + const [username, setUsername] = React.useState(initialUsername) + const [errorText, setErrorText] = React.useState(error === 'Input canceled' ? '' : error) + + React.useEffect(() => { + setUsername(initialUsername) + }, [initialUsername]) + + React.useEffect(() => { + setErrorText(error === 'Input canceled' ? '' : error) + }, [error]) + + const canSubmit = !!username.length + const submit = () => { + if (!canSubmit) { + return + } + setErrorText('') + onSubmit(platform, username) + } + + const pt = platformText[platform] + if (!pt.headerText) { + throw new Error(`Proofs for platform ${platform} are unsupported.`) + } + + return ( + + {!!errorText && ( + + + {errorText} + + + )} + + {C.isMobile ? null : ( + + {pt.headerText} + + )} + + + + + + + + + + ) } -export type Props = { +const GenericEnterUsername = ({ + onCancel, + onSubmit, + step, +}: { onCancel: () => void - providerClicked: (key: string) => void - providers: Array - title: string + onSubmit: (username: string) => void + step: GenericEnterUsernameStep +}) => { + const [username, setUsername] = React.useState(step.username) + React.useEffect(() => { + setUsername(step.username) + }, [step.username]) + + const unreachable = !!step.proofUrl + return ( + + {!unreachable && !Kb.Styles.isMobile && } + + + + + + + {step.genericParams.title} + + {step.genericParams.subtext} + + + + + {unreachable ? ( + + ) : ( + + + + {step.genericParams.suffix} + + } + error={!!step.error} + onChangeText={setUsername} + onEnterKeyDown={() => onSubmit(username)} + placeholder={ + step.genericParams.suffix === '@theqrl.org' ? 'Your QRL address' : 'Your username' + } + value={username} + /> + + )} + {!!step.error && {step.error}} + + + {unreachable && ( + + You need to authorize your proof on {step.genericParams.title}. + + )} + + {!Kb.Styles.isMobile && !unreachable && ( + + )} + {unreachable ? ( + onSubmit(username)} + label={step.genericParams.buttonLabel} + style={styles.buttonBig} + /> + ) : ( + onSubmit(username)} + label={step.genericParams.buttonLabel} + style={styles.buttonBig} + waitingKey={C.waitingKeyProfile} + /> + )} + + + + ) } -type ProvidersProps = { - filter: string -} & Props +const PostProof = ({ + copyToClipboard, + onCancel, + onSubmit, + step, +}: { + copyToClipboard: (text: string) => void + onCancel: () => void + onSubmit: () => void + step: PostProofStep +}) => { + let proofText = step.proofText + let url = '' + let openLinkBeforeSubmit = false + switch (step.platform) { + case 'twitter': + openLinkBeforeSubmit = true + url = proofText ? `https://twitter.com/home?status=${proofText}` : '' + break + case 'github': + openLinkBeforeSubmit = true + url = 'https://gist.github.com/' + break + case 'reddit': + case 'facebook': + openLinkBeforeSubmit = true + url = proofText + proofText = '' + break + case 'hackernews': + openLinkBeforeSubmit = true + url = `https://news.ycombinator.com/user?id=${step.username}` + break + default: + break + } -function Providers({filter, providerClicked, providers}: ProvidersProps) { - const _itemHeight = { - height: Kb.Styles.isMobile ? 56 : 48, - type: 'fixed', - } as const + const [showSubmit, setShowSubmit] = React.useState(!openLinkBeforeSubmit) + const platformSubtitle = subtitle(step.platform) + const proofActionText = actionMap.get(step.platform) ?? '' + const onCompleteText = checkMap.get(step.platform) ?? 'OK posted! Check for it!' + const noteText = noteMap.get(step.platform) ?? '' + const DescriptionView = descriptionMap[step.platform] - const _renderItem = (_: unknown, provider: IdentityProvider) => ( - - - providerClicked(provider.key)} - style={styles.containerBox} + return ( + + - - - - {provider.name} - - {(provider.new || !!provider.desc) && ( - - {provider.new && ( - - )} - - {provider.desc} + { + e.preventDefault() + proofText && copyToClipboard(proofText) + }} + > + {!!step.error && ( + + + {step.error} )} + + <> + + {step.username} + + {!!platformSubtitle && ( + + {platformSubtitle} + + )} + + + {!!proofText && } + {!!noteText && ( + + {noteText} + + )} + + + {showSubmit ? ( + + ) : ( + { + setShowSubmit(true) + url && openUrl(url) + }} + label={proofActionText} + /> + )} + - + + ) +} + +const ConfirmOrPending = ({ + onClose, + step, +}: { + onClose: () => void + step: ConfirmOrPendingStep +}) => { + const isGood = step.proofFound && step.proofStatus === T.RPCGen.ProofStatus.ok + const isPending = + !isGood && + !step.proofFound && + !!step.proofStatus && + step.proofStatus <= T.RPCGen.ProofStatus.baseHardError + const platformIconOverlayColor = isGood ? Kb.Styles.globalColors.green : Kb.Styles.globalColors.greyDark + const platformIconOverlay = isPending ? 'icon-proof-pending' : 'icon-proof-success' + const platformSubtitle = subtitle(step.platform) + const title = isPending ? 'Your proof is pending.' : 'Verified!' + const message = + messageMap.get(step.platform) || + (isPending + ? 'Some proofs can take a few hours to recognize. Check back later.' + : 'Leave your proof up so other users can identify you!') + + return ( + + + + {title} + + - - + <> + + {step.username} + + {platformSubtitle && ( + + {platformSubtitle} + + )} + + + {message} + + {step.platform === 'http' && ( + + Note: {step.username} doesn't load over https. If you get a real SSL certificate + (not self-signed) in the future, please replace this proof with a fresh one. + + )} + + + ) +} - const filterRegexp = makeInsertMatcher(filter) +const GenericResult = ({ + onClose, + step, +}: { + onClose: () => void + step: GenericResultStep +}) => { + const proofUsername = step.username + step.genericParams.suffix + const success = !step.error + const iconType = success ? 'icon-proof-success' : 'icon-proof-broken' + return ( + + + + + + + + + {success ? ( + <> + You are provably + {proofUsername} + + ) : ( + {step.error} + )} + + + + + + ) +} - const items = (() => { - const exact: Array = [] - const inexact: Array = [] - providers.forEach(p => { - if (p.name === filter) { - exact.push(p) - } else if (filterProvider(p, filterRegexp)) { - inexact.push(p) - } - }) - return [...exact, ...inexact] - })() +const Unreachable = ({ + serviceIcon, + serviceSuffix, + username, +}: { + serviceIcon: T.Tracker.SiteIconSet + serviceSuffix: string + username: string +}) => ( + + + + + + {username} + + {serviceSuffix} + + + + + + + +) +const UsernameTips = ({platform}: {platform: T.More.PlatformsExpandedType}) => + platform === 'hackernews' ? ( + + • You must have karma ≥ 2 + • You must enter your uSeRName with exact case + + ) : null + +const standardText = (name: string) => ({ + headerText: C.isMobile ? `Prove ${name}` : `Prove your ${name} identity`, + hintText: `Your ${name} username`, +}) + +const invalidText = () => ({ + headerText: '', + hintText: '', +}) + +const platformText = { + btc: {headerText: 'Set a Bitcoin address', hintText: 'Your Bitcoin address'}, + dns: {headerText: 'Prove your domain', hintText: 'yourdomain.com'}, + dnsOrGenericWebSite: invalidText(), + facebook: standardText('Facebook'), + github: standardText('GitHub'), + hackernews: standardText('Hacker News'), + http: {headerText: 'Prove your http website', hintText: 'http://whatever.yoursite.com'}, + https: {headerText: 'Prove your https website', hintText: 'https://whatever.yoursite.com'}, + pgp: invalidText(), + reddit: standardText('Reddit'), + rooter: invalidText(), + twitter: standardText('Twitter'), + web: {headerText: 'Prove your website', hintText: 'whatever.yoursite.com'}, + zcash: {headerText: 'Set a Zcash address', hintText: 'Your z_address or t_address'}, +} + +const actionMap = new Map([ + ['github', 'Create gist now'], + ['hackernews', 'Go to Hacker News'], + ['reddit', 'Reddit form'], + ['twitter', 'Tweet it now'], +]) + +const checkMap = new Map([['twitter', 'OK tweeted! Check for it!']]) + +const webNote = 'Note: If someone already verified this domain, just append to the existing keybase.txt file.' + +const noteMap = new Map([ + ['http', webNote], + ['https', webNote], + ['reddit', "Make sure you're signed in to Reddit, and don't edit the text or title before submitting."], + ['web', webNote], +]) + +const WebDescription = ({platformUserName}: {platformUserName: string}) => { + const root = `${platformUserName}/keybase.txt` + const wellKnown = `${platformUserName}/.well-known/keybase.txt` + const rootUrlProps = Kb.useClickURL(`https://${root}`) + const wellKnownUrlProps = Kb.useClickURL(`https://${wellKnown}`) return ( - - - + + + Please serve the text below exactly as it appears + {" at one of these URL's."} + + + {root} + + + {wellKnown} + + ) } -const normalizeForFiltering = (input: string) => input.toLowerCase().replace(/[.\s]/g, '') +const descriptionMap = { + dns: () => ( + + Enter the following as a TXT entry in your DNS zone, exactly as it appears + {'. If you need a "name" for your entry, give it "@".'} + + ), + facebook: () => null, + github: () => ( + + Login to GitHub and paste the text below into a public gist + called keybase.md. + + ), + hackernews: () => ( + + Please add the below text exactly as it appears{' '} + to your profile. + + ), + http: WebDescription, + https: WebDescription, + reddit: () => ( + + Click the button below and post the form in the subreddit KeybaseProofs. + + ), + rooter: () => null, + twitter: () => ( + + Click the button below and tweet the text exactly as it appears. + + ), + web: WebDescription, +} satisfies Record> -const filterProvider = (p: IdentityProvider, filter: RegExp) => +const messageMap = new Map([ + ['btc', 'Your Bitcoin address has now been signed onto your profile.'], + ['dns', 'DNS proofs can take a few hours to recognize. Check back later.'], + [ + 'hackernews', + 'Hacker News caches its bios, so it might be a few hours before you can verify your proof. Check back later.', + ], + ['zcash', 'Your Zcash address has now been signed onto your profile.'], +]) + +const normalizeForFiltering = (input: string) => input.toLowerCase().replace(/[.\s]/g, '') +const filterProvider = (p: Provider, filter: RegExp) => normalizeForFiltering(p.name).search(filter) !== -1 || normalizeForFiltering(p.desc).search(filter) !== -1 const rightColumnStyle = Kb.Styles.platformStyles({ @@ -157,6 +1257,29 @@ const rightColumnStyle = Kb.Styles.platformStyles({ const styles = Kb.Styles.styleSheetCreate( () => ({ + backButton: { + left: Kb.Styles.globalMargins.small, + position: 'absolute', + top: Kb.Styles.globalMargins.small, + }, + blue: Kb.Styles.platformStyles({ + common: {color: Kb.Styles.globalColors.blueDark}, + isElectron: {wordBreak: 'break-all'}, + }), + bottomContainer: {height: 80}, + buttonBar: { + ...Kb.Styles.padding( + Kb.Styles.globalMargins.small, + Kb.Styles.globalMargins.medium, + Kb.Styles.globalMargins.medium + ), + }, + buttonBarWarning: {backgroundColor: Kb.Styles.globalColors.yellow}, + buttonBig: {flex: 2.5}, + buttonSmall: {flex: 1}, + centered: {alignSelf: 'center'}, + center: {alignSelf: 'center'}, + colorRed: {color: Kb.Styles.globalColors.redDark}, container: Kb.Styles.platformStyles({ common: {flex: 1}, isElectron: { @@ -175,6 +1298,13 @@ const styles = Kb.Styles.styleSheetCreate( justifyContent: 'flex-start', }, description: {...rightColumnStyle}, + error: { + backgroundColor: Kb.Styles.globalColors.red, + borderRadius: Kb.Styles.borderRadius, + marginBottom: Kb.Styles.globalMargins.small, + padding: Kb.Styles.globalMargins.medium, + }, + grey: {color: Kb.Styles.globalColors.black_20}, icon: { height: 32, marginLeft: Kb.Styles.globalMargins.small, @@ -182,6 +1312,23 @@ const styles = Kb.Styles.styleSheetCreate( width: 32, }, iconArrow: {marginRight: Kb.Styles.globalMargins.small}, + iconBadgeContainer: { + bottom: -5, + position: 'absolute', + right: -5, + }, + inlineIcon: { + position: 'relative', + top: 1, + }, + inputBox: { + ...Kb.Styles.padding(Kb.Styles.globalMargins.small, Kb.Styles.globalMargins.small), + borderColor: Kb.Styles.globalColors.black_10, + borderRadius: Kb.Styles.borderRadius, + borderStyle: 'solid' as const, + borderWidth: 1, + minHeight: 40, + }, inputContainer: { alignSelf: 'stretch', backgroundColor: Kb.Styles.globalColors.black_10, @@ -193,21 +1340,74 @@ const styles = Kb.Styles.styleSheetCreate( padding: Kb.Styles.globalMargins.tiny, width: 'auto', }, + inputContainerWrap: { + ...Kb.Styles.padding(Kb.Styles.globalMargins.medium), + }, listContainer: {flex: 1}, + marginLeftAuto: {marginLeft: 'auto'}, mobileFlex: {flex: 1}, new: { marginRight: Kb.Styles.globalMargins.xtiny, marginTop: 1, }, + opacity40: {opacity: 0.4}, + opacity75: {opacity: 0.75}, + placeholderService: { + color: Kb.Styles.globalColors.black_50, + }, + proof: { + maxWidth: '100%', + }, + providerButtonBar: { + padding: Kb.Styles.globalMargins.medium, + }, + scroll: {maxWidth: '100%'}, + scrollContent: { + paddingBottom: Kb.Styles.globalMargins.small, + }, + serviceIconContainer: { + marginBottom: Kb.Styles.globalMargins.tiny, + position: 'relative', + }, + serviceIconFull: { + height: 64, + width: 64, + }, + serviceIconHeaderContainer: { + marginTop: Kb.Styles.globalMargins.medium, + }, + serviceMeta: { + maxWidth: 320, + }, + serviceProofIcon: { + bottom: -8, + position: 'absolute', + right: -8, + }, text: { backgroundColor: Kb.Styles.globalColors.transparent, color: Kb.Styles.globalColors.black_50, marginRight: Kb.Styles.globalMargins.tiny, }, + tips: {padding: Kb.Styles.globalMargins.small}, title: { ...rightColumnStyle, color: Kb.Styles.globalColors.black, }, + topContainer: {flex: 1}, + unreachableBox: { + backgroundColor: Kb.Styles.globalColors.black_05, + }, + unreachablePlaceholder: { + alignItems: 'center', + display: 'flex', + flexDirection: 'row', + flexWrap: 'wrap', + gap: Kb.Styles.globalMargins.xtiny, + }, + warningText: { + ...Kb.Styles.padding(Kb.Styles.globalMargins.small, Kb.Styles.globalMargins.medium, 0), + }, }) as const ) diff --git a/shared/profile/prove-website-choice.tsx b/shared/profile/prove-website-choice.tsx index ff6e664c9f14..9391d7846e0b 100644 --- a/shared/profile/prove-website-choice.tsx +++ b/shared/profile/prove-website-choice.tsx @@ -1,21 +1,18 @@ import * as C from '@/constants' -import {useProfileState} from '@/stores/profile' import * as Kb from '@/common-adapters' import Modal from './modal' const ProveWebsiteChoice = () => { - const cancelAddProof = useProfileState(s => s.dispatch.dynamic.cancelAddProof) - const addProof = useProfileState(s => s.dispatch.addProof) - const clearModals = C.useRouterState(s => s.dispatch.clearModals) + const navigateAppend = C.useRouterState(s => s.dispatch.navigateAppend) + const navigateUp = C.useRouterState(s => s.dispatch.navigateUp) const onCancel = () => { - cancelAddProof?.() - clearModals() + navigateUp() } const onDNS = () => { - addProof('dns', 'profile') + navigateAppend({name: 'profileProofsList', params: {platform: 'dns'}}) } const onFile = () => { - addProof('web', 'profile') + navigateAppend({name: 'profileProofsList', params: {platform: 'web'}}) } return ( diff --git a/shared/stores/profile.tsx b/shared/stores/profile.tsx index e09d7df2d9f2..bb1b45d20a50 100644 --- a/shared/stores/profile.tsx +++ b/shared/stores/profile.tsx @@ -1,14 +1,9 @@ import * as T from '@/constants/types' -import {generateGUIID, ignorePromise, wrapErrors} from '@/constants/utils' -import * as S from '@/constants/strings' +import {ignorePromise, wrapErrors} from '@/constants/utils' import * as Z from '@/util/zustand' -import logger from '@/logger' -import {openURL} from '@/util/misc' import {RPCError} from '@/util/errors' -import {getVisibleScreen, navigateAppend} from '@/constants/router' -import {useCurrentUserState} from '@/stores/current-user' +import {navigateAppend} from '@/constants/router' import type {useTrackerState} from '@/stores/tracker' -import {normalizeProofUsername} from '@/profile/proof-utils' export type ProveGenericParams = { logoBlack: T.Tracker.SiteIconSet @@ -67,7 +62,6 @@ export type State = Store & { finishedWithKeyGen?: (shouldStoreKeyOnServer: boolean) => void submitUsername?: (username: string) => void } - addProof: (platform: string, reason: 'appLink' | 'profile') => void editAvatar: () => void generatePgp: (args: GeneratePgpArgs) => void resetState: () => void @@ -75,16 +69,6 @@ export type State = Store & { } export const useProfileState = Z.createZustand('profile', (set, get) => { - const addProofRouteNames = new Set([ - 'profileConfirmOrPending', - 'profileGenericEnterUsername', - 'profileGenericProofResult', - 'profilePostProof', - 'profileProofsList', - 'profileProveEnterUsername', - 'profileProveWebsiteChoice', - ]) - const resetProofCallbacks = () => { set(s => { s.dispatch.dynamic.afterCheckProof = undefined @@ -93,7 +77,6 @@ export const useProfileState = Z.createZustand('profile', (set, get) => { }) } const defaultCancelAddProof = () => { - addProofInProgress = false resetProofCallbacks() } @@ -104,300 +87,7 @@ export const useProfileState = Z.createZustand('profile', (set, get) => { }) } - const shouldReplacePrompt = (name: 'profileGenericEnterUsername' | 'profileProveEnterUsername') => - getVisibleScreen()?.name === name - - const clearStaleAddProofGuard = () => { - if (!addProofInProgress) { - return - } - const visibleScreen = getVisibleScreen()?.name - if (visibleScreen && addProofRouteNames.has(visibleScreen)) { - return - } - logger.info('clearing stale addProof guard', {visibleScreen}) - defaultCancelAddProof() - } - - const checkProofAndNavigate = async ( - platform: T.More.PlatformsExpandedType, - sigID: T.RPCGen.SigID, - username: string, - proofText: string - ) => { - try { - const {found, status} = await T.RPCGen.proveCheckProofRpcPromise({sigID}, S.waitingKeyProfile) - if (!found && status >= T.RPCGen.ProofStatus.baseHardError) { - navigateAppend( - { - name: 'profilePostProof', - params: { - error: "We couldn't find your proof. Please retry!", - platform, - proofText, - username, - }, - }, - true - ) - } else { - navigateAppend({ - name: 'profileConfirmOrPending', - params: { - platform, - proofFound: found, - proofStatus: status, - username, - }, - }) - } - } catch { - logger.warn('Error getting proof update') - navigateAppend( - { - name: 'profilePostProof', - params: { - error: "We couldn't verify your proof. Please retry!", - platform, - proofText, - username, - }, - }, - true - ) - } - } - - let addProofInProgress = false - const dispatch: State['dispatch'] = { - addProof: (platform, reason) => { - clearStaleAddProofGuard() - if (addProofInProgress) { - logger.warn('addProof while one in progress') - return - } - - const service = T.More.asPlatformsExpandedType(platform) - const genericService = service ? null : platform - - switch (service) { - case 'dnsOrGenericWebSite': - navigateAppend('profileProveWebsiteChoice') - return - case 'zcash': - case 'btc': - navigateAppend({name: 'profileProveEnterUsername', params: {platform: service}}) - return - case 'pgp': - navigateAppend('profilePgp') - return - default: - break - } - - addProofInProgress = true - - const inputCancelError = { - code: T.RPCGen.StatusCode.scinputcanceled, - desc: 'Cancel Add Proof', - } - - let canceled = false - let currentUsername = '' - let currentGenericParams = makeProveGenericParams() - let currentProofText = '' - - const loadAfter = () => - get().dispatch.defer.onTracker2Load?.({ - assertion: useCurrentUserState.getState().username, - guiID: generateGUIID(), - inTracker: false, - reason: '', - }) - - const f = async () => { - try { - const {sigID} = await T.RPCGen.proveStartProofRpcListener({ - customResponseIncomingCallMap: { - 'keybase.1.proveUi.checking': (_, response) => { - if (canceled) { - response.error(inputCancelError) - return - } - response.result() - }, - 'keybase.1.proveUi.continueChecking': (_, response) => - canceled ? response.result(false) : response.result(true), - 'keybase.1.proveUi.okToCheck': (_, response) => response.result(true), - 'keybase.1.proveUi.outputInstructions': ({proof}, response) => { - if (canceled) { - response.error(inputCancelError) - return - } - set(s => { - s.dispatch.dynamic.afterCheckProof = wrapErrors(() => { - set(s => { - s.dispatch.dynamic.afterCheckProof = undefined - }) - response.result() - }) - s.dispatch.dynamic.cancelAddProof = wrapErrors(() => { - resetProofCallbacks() - canceled = true - response.error(inputCancelError) - }) - }) - - if (service && proof) { - currentProofText = proof - navigateAppend({ - name: 'profilePostProof', - params: { - platform: service, - proofText: proof, - username: currentUsername, - }, - }) - } else if (proof) { - navigateAppend( - { - name: 'profileGenericEnterUsername', - params: { - genericParams: currentGenericParams, - proofUrl: proof, - service: genericService ?? '', - username: currentUsername, - }, - }, - true - ) - openURL(proof) - get().dispatch.dynamic.afterCheckProof?.() - } - }, - 'keybase.1.proveUi.preProofWarning': (_, response) => response.result(true), - 'keybase.1.proveUi.promptOverwrite': (_, response) => response.result(true), - 'keybase.1.proveUi.promptUsername': (args, response) => { - const {parameters, prevError} = args - if (canceled) { - response.error(inputCancelError) - return - } - set(s => { - s.dispatch.dynamic.cancelAddProof = wrapErrors(() => { - resetProofCallbacks() - canceled = true - response.error(inputCancelError) - }) - s.dispatch.dynamic.submitUsername = wrapErrors((username: string) => { - const {normalized} = normalizeProofUsername(service, username) - currentUsername = normalized - set(s => { - s.dispatch.dynamic.submitUsername = undefined - }) - response.result(normalized) - }) - }) - - if (service) { - navigateAppend( - { - name: 'profileProveEnterUsername', - params: { - error: prevError?.desc, - platform: service, - username: currentUsername || undefined, - }, - }, - shouldReplacePrompt('profileProveEnterUsername') - ) - } else if (genericService && parameters) { - currentGenericParams = toProveGenericParams(parameters) - navigateAppend( - { - name: 'profileGenericEnterUsername', - params: { - error: prevError?.desc, - genericParams: currentGenericParams, - service: genericService, - username: currentUsername || undefined, - }, - }, - shouldReplacePrompt('profileGenericEnterUsername') - ) - } - }, - }, - incomingCallMap: { - 'keybase.1.proveUi.displayRecheckWarning': () => {}, - 'keybase.1.proveUi.outputPrechecks': () => {}, - }, - params: { - auto: false, - force: true, - promptPosted: !!genericService, - service: platform, - username: '', - }, - waitingKey: S.waitingKeyProfile, - }) - - logger.info('Start Proof done: ', sigID) - loadAfter() - - if (service) { - ignorePromise(checkProofAndNavigate(service, sigID, currentUsername, currentProofText)) - } else { - navigateAppend( - { - name: 'profileGenericProofResult', - params: { - genericParams: currentGenericParams, - username: currentUsername, - }, - }, - true - ) - } - } catch (_error) { - loadAfter() - if (_error instanceof RPCError) { - const error = _error - logger.warn('Error making proof') - - if (genericService) { - navigateAppend( - { - name: 'profileGenericProofResult', - params: { - error: error.desc || 'Failed to verify proof', - genericParams: currentGenericParams, - username: currentUsername, - }, - }, - true - ) - } - - if (error.code === T.RPCGen.StatusCode.scgeneric && reason === 'appLink') { - navigateAppend({ - name: 'keybaseLinkError', - params: { - error: - "We couldn't find a valid service for proofs in this link. The link might be bad, or your Keybase app might be out of date and need to be updated.", - }, - }) - } - } - } finally { - addProofInProgress = false - resetProofCallbacks() - resetPgpCallbacks() - } - } - ignorePromise(f()) - }, defer: { onTracker2GetDetails: () => { throw new Error('onTracker2GetDetails not implemented') diff --git a/shared/tracker/assertion.tsx b/shared/tracker/assertion.tsx index b18963e91549..0163cfc1c34d 100644 --- a/shared/tracker/assertion.tsx +++ b/shared/tracker/assertion.tsx @@ -10,7 +10,6 @@ import {formatTimeForAssertionPopup} from '@/util/timestamp' import {useColorScheme} from 'react-native' import * as Tracker from '@/stores/tracker' import {useTrackerState} from '@/stores/tracker' -import {useProfileState} from '@/stores/profile' import {generateGUIID} from '@/constants/utils' import {navToProfile} from '@/constants/router' @@ -76,13 +75,11 @@ const Container = (ownProps: OwnProps) => { ) const {color, metas: _metas, proofURL, sigID, siteIcon, stellarHidden, notAUser} = data const {siteIconDarkmode, siteIconFull, siteIconFullDarkmode, siteURL, state, timestamp, type, value} = data - const addProof = useProfileState(s => s.dispatch.addProof) const loadProfile = useTrackerState(s => s.dispatch.load) const hideStellar = C.useRPC(T.RPCGen.apiserverPostRpcPromise) const recheckProof = C.useRPC(T.RPCGen.proveCheckProofRpcPromise) - const _onCreateProof = () => { - addProof(type, 'profile') - } + const navigateAppend = C.useRouterState(s => s.dispatch.navigateAppend) + const _onCreateProof = () => navigateAppend({name: 'profileProofsList', params: {platform: type}}) const onHideStellar = (hidden: boolean) => { hideStellar( [{args: [{key: 'hidden', value: hidden ? '1' : '0'}], endpoint: 'stellar/hidden'}, C.waitingKeyTracker], @@ -100,9 +97,6 @@ const Container = (ownProps: OwnProps) => { () => {} ) } - - const navigateAppend = C.useRouterState(s => s.dispatch.navigateAppend) - const _onRevoke = () => { navigateAppend({ name: 'profileRevoke', From 6d6ac993e98de1ad5da0ddd620faadc4ce26e3bd Mon Sep 17 00:00:00 2001 From: Chris Nojima Date: Fri, 27 Mar 2026 17:10:15 -0400 Subject: [PATCH 11/14] WIP --- shared/constants/init/index.desktop.tsx | 10 - shared/constants/init/index.native.tsx | 27 --- shared/constants/init/shared.tsx | 28 --- shared/people/todo.tsx | 4 +- shared/profile/confirm-or-pending.tsx | 97 -------- shared/profile/generic/enter-username.tsx | 272 ---------------------- shared/profile/generic/result.tsx | 89 ------- shared/profile/post-proof.tsx | 253 -------------------- shared/profile/prove-enter-username.tsx | 223 ------------------ shared/profile/routes.tsx | 15 -- shared/profile/user/hooks.tsx | 3 +- shared/stores/profile.tsx | 85 +------ shared/stores/tests/profile.test.ts | 7 +- shared/util/misc.d.ts | 1 + shared/util/misc.desktop.tsx | 5 + shared/util/misc.native.tsx | 24 +- 16 files changed, 34 insertions(+), 1109 deletions(-) delete mode 100644 shared/profile/confirm-or-pending.tsx delete mode 100644 shared/profile/generic/enter-username.tsx delete mode 100644 shared/profile/generic/result.tsx delete mode 100644 shared/profile/post-proof.tsx delete mode 100644 shared/profile/prove-enter-username.tsx diff --git a/shared/constants/init/index.desktop.tsx b/shared/constants/init/index.desktop.tsx index 2b8e4f929246..02aed5d0d896 100644 --- a/shared/constants/init/index.desktop.tsx +++ b/shared/constants/init/index.desktop.tsx @@ -5,8 +5,6 @@ import {useConfigState} from '@/stores/config' import * as ConfigConstants from '@/stores/config' import {useDaemonState} from '@/stores/daemon' import {useFSState} from '@/stores/fs' -import {useProfileState} from '@/stores/profile' -import {useRouterState} from '@/stores/router' import type * as EngineGen from '@/constants/rpc' import * as T from '@/constants/types' import InputMonitor from '@/util/platform-specific/input-monitor.desktop' @@ -410,14 +408,6 @@ export const initPlatformListener = () => { } })) - useProfileState.setState(s => { - s.dispatch.editAvatar = () => { - useRouterState - .getState() - .dispatch.navigateAppend({name: 'profileEditAvatar', params: {image: undefined}}) - } - }) - useDaemonState.setState(s => { s.dispatch.onRestartHandshakeNative = () => { const {handshakeFailedReason} = useDaemonState.getState() diff --git a/shared/constants/init/index.native.tsx b/shared/constants/init/index.native.tsx index 14a145c5aee8..a7359075146f 100644 --- a/shared/constants/init/index.native.tsx +++ b/shared/constants/init/index.native.tsx @@ -6,7 +6,6 @@ import {useCurrentUserState} from '@/stores/current-user' import {useDaemonState} from '@/stores/daemon' import {useDarkModeState} from '@/stores/darkmode' import {useFSState} from '@/stores/fs' -import {useProfileState} from '@/stores/profile' import {useRouterState} from '@/stores/router' import {useSettingsContactsState} from '@/stores/settings-contacts' import * as T from '@/constants/types' @@ -22,7 +21,6 @@ import {Alert, Linking} from 'react-native' import {isAndroid} from '@/constants/platform.native' import {wrapErrors} from '@/util/debug' import {getTab, getVisiblePath, logState, switchTab} from '@/constants/router' -import {launchImageLibraryAsync} from '@/util/expo-image-picker.native' import {pickDocumentsAsync} from '@/util/expo-document-picker.native' import {setupAudioMode} from '@/util/audio.native' import { @@ -35,7 +33,6 @@ import { } from 'react-native-kb' import {initPushListener, getStartupDetailsFromInitialPush} from './push-listener.native' import {initSharedSubscriptions, _onEngineIncoming} from './shared' -import type {ImageInfo} from '@/util/expo-image-picker.native' import {noConversationIDKey} from '../types/chat/common' import {getSelectedConversation} from '../chat/common' import {getConvoState} from '@/stores/convostate' @@ -398,30 +395,6 @@ export const initPlatformListener = () => { }) }) - useProfileState.setState(s => { - s.dispatch.editAvatar = () => { - const f = async () => { - try { - const result = await launchImageLibraryAsync('photo') - const first = result.assets?.reduce((acc, a) => { - if (!acc && (a.type === 'image' || a.type === 'video')) { - return a as ImageInfo - } - return acc - }, undefined) - if (!result.canceled && first) { - useRouterState - .getState() - .dispatch.navigateAppend({name: 'profileEditAvatar', params: {image: first}}) - } - } catch (error) { - useConfigState.getState().dispatch.filePickerError(new Error(String(error))) - } - } - ignorePromise(f()) - } - }) - useConfigState.subscribe((s, old) => { if (s.loggedIn === old.loggedIn) return const f = async () => { diff --git a/shared/constants/init/shared.tsx b/shared/constants/init/shared.tsx index ac08929b0d9f..b58948c28048 100644 --- a/shared/constants/init/shared.tsx +++ b/shared/constants/init/shared.tsx @@ -51,7 +51,6 @@ import {useFSState} from '@/stores/fs' import {useFollowerState} from '@/stores/followers' import {useModalHeaderState} from '@/stores/modal-header' import {useNotifState} from '@/stores/notifications' -import {useProfileState} from '@/stores/profile' import {useProvisionState} from '@/stores/provision' import {usePushState} from '@/stores/push' import {useSettingsContactsState} from '@/stores/settings-contacts' @@ -295,32 +294,6 @@ export const initNotificationsCallbacks = () => { }) } -export const initProfileCallbacks = () => { - const currentState = useProfileState.getState() - useProfileState.setState({ - dispatch: { - ...currentState.dispatch, - defer: { - ...currentState.dispatch.defer, - onTracker2GetDetails: (username: string) => { - return useTrackerState.getState().getDetails(username) - }, - onTracker2Load: ( - params: Parameters['dispatch']['load']>[0] - ) => { - useTrackerState.getState().dispatch.load(params) - }, - onTracker2ShowUser: (username: string, asTracker: boolean, skipNav?: boolean) => { - useTrackerState.getState().dispatch.showUser(username, asTracker, skipNav) - }, - onTracker2UpdateResult: (guiID: string, result: T.Tracker.DetailsState, reason?: string) => { - useTrackerState.getState().dispatch.updateResult(guiID, result, reason) - }, - }, - }, - }) -} - export const initPushCallbacks = () => { const currentState = usePushState.getState() usePushState.setState({ @@ -705,7 +678,6 @@ export const initSharedSubscriptions = () => { initTeamsCallbacks() initFSCallbacks() initNotificationsCallbacks() - initProfileCallbacks() initPushCallbacks() initRecoverPasswordCallbacks() initSettingsCallbacks() diff --git a/shared/people/todo.tsx b/shared/people/todo.tsx index 97385f988ce4..8574a300d96d 100644 --- a/shared/people/todo.tsx +++ b/shared/people/todo.tsx @@ -1,7 +1,7 @@ import * as C from '@/constants' import {useTeamsState} from '@/stores/teams' import * as React from 'react' -import {openURL} from '@/util/misc' +import {editAvatar, openURL} from '@/util/misc' import type * as T from '@/constants/types' import type {IconType} from '@/common-adapters/icon.constants-gen' import PeopleItem, {type TaskButton} from './item' @@ -11,7 +11,6 @@ import {settingsAccountTab, settingsGitTab} from '@/constants/settings' import {useTrackerState} from '@/stores/tracker' import {useCurrentUserState} from '@/stores/current-user' import {navToProfile} from '@/constants/router' -import {useProfileState} from '@/stores/profile' const todoTypes: {[K in T.People.TodoType]: T.People.TodoType} = { addEmail: 'addEmail', @@ -115,7 +114,6 @@ const AvatarTeamConnector = (props: TodoOwnProps) => { } const AvatarUserConnector = (props: TodoOwnProps) => { - const editAvatar = useProfileState(s => s.dispatch.editAvatar) const onConfirm = editAvatar const buttons = makeDefaultButtons(onConfirm, props.confirmLabel) return diff --git a/shared/profile/confirm-or-pending.tsx b/shared/profile/confirm-or-pending.tsx deleted file mode 100644 index 2b609f95a91c..000000000000 --- a/shared/profile/confirm-or-pending.tsx +++ /dev/null @@ -1,97 +0,0 @@ -import * as T from '@/constants/types' -import * as Kb from '@/common-adapters' -import {subtitle} from '@/util/platforms' -import Modal from './modal' -import {useCurrentUserState} from '@/stores/current-user' -import * as C from '@/constants' -import {navToProfile} from '@/constants/router' - -type Props = { - platform: T.More.PlatformsExpandedType - proofFound: boolean - proofStatus?: T.RPCGen.ProofStatus - username: string -} - -const ConfirmOrPending = ({platform, proofFound, proofStatus, username}: Props) => { - const currentUsername = useCurrentUserState(s => s.username) - const clearModals = C.useRouterState(s => s.dispatch.clearModals) - - const isGood = proofFound && proofStatus === T.RPCGen.ProofStatus.ok - const isPending = - !isGood && !proofFound && !!proofStatus && proofStatus <= T.RPCGen.ProofStatus.baseHardError - - const platformIconOverlayColor = isGood ? Kb.Styles.globalColors.green : Kb.Styles.globalColors.greyDark - const onCancel = () => { - clearModals() - navToProfile(currentUsername) - } - - const message = - messageMap.get(platform) || - (isPending - ? 'Some proofs can take a few hours to recognize. Check back later.' - : 'Leave your proof up so other users can identify you!') - const platformIconOverlay = isPending ? 'icon-proof-pending' : 'icon-proof-success' - const platformSubtitle = subtitle(platform) - const title = isPending ? 'Your proof is pending.' : 'Verified!' - - return ( - - - - {title} - - - <> - - {username} - - {platformSubtitle && ( - - {platformSubtitle} - - )} - - <> - - {message} - - {platform === 'http' && ( - - Note: {username} doesn't load over https. If you get a real SSL certificate (not - self-signed) in the future, please replace this proof with a fresh one. - - )} - - - - - ) -} - -const styles = Kb.Styles.styleSheetCreate(() => ({ - blue: Kb.Styles.platformStyles({ - common: {color: Kb.Styles.globalColors.blueDark}, - isElectron: {wordBreak: 'break-all'}, - }), - center: {alignSelf: 'center'}, - grey: {color: Kb.Styles.globalColors.black_20}, -})) - -const messageMap = new Map([ - ['btc', 'Your Bitcoin address has now been signed onto your profile.'], - ['dns', 'DNS proofs can take a few hours to recognize. Check back later.'], - [ - 'hackernews', - 'Hacker News caches its bios, so it might be a few hours before you can verify your proof. Check back later.', - ], - ['zcash', 'Your Zcash address has now been signed onto your profile.'], -]) - -export default ConfirmOrPending diff --git a/shared/profile/generic/enter-username.tsx b/shared/profile/generic/enter-username.tsx deleted file mode 100644 index b211ea75f609..000000000000 --- a/shared/profile/generic/enter-username.tsx +++ /dev/null @@ -1,272 +0,0 @@ -import * as C from '@/constants' -import {type ProveGenericParams, useProfileState} from '@/stores/profile' -import {openURL} from '@/util/misc' -import * as React from 'react' -import * as Kb from '@/common-adapters' -import {SiteIcon} from './shared' -import type * as T from '@/constants/types' - -type Props = { - error?: string - genericParams: ProveGenericParams - proofUrl?: string - service: string - username?: string -} - -const ConnectedEnterUsername = ({error = '', genericParams, proofUrl, username: routeUsername = ''}: Props) => { - const serviceIcon = genericParams.logoBlack - const serviceIconFull = genericParams.logoFull - const serviceName = genericParams.title - const serviceSub = genericParams.subtext - const serviceSuffix = genericParams.suffix - const submitButtonLabel = genericParams.buttonLabel - const unreachable = !!proofUrl - - const cancelAddProof = useProfileState(s => s.dispatch.dynamic.cancelAddProof) - const submitUsername = useProfileState(s => s.dispatch.dynamic.submitUsername) - - const clearModals = C.useRouterState(s => s.dispatch.clearModals) - const onBack = () => { - cancelAddProof?.() - clearModals() - } - const [username, setUsername] = React.useState(routeUsername) - const onSubmit = proofUrl ? () => openURL(proofUrl) : () => submitUsername?.(username) - - React.useEffect(() => { - setUsername(routeUsername) - }, [routeUsername]) - - return ( - <> - - {!unreachable && !Kb.Styles.isMobile && } - - - - - - - {serviceName} - - {serviceSub} - - - - - {unreachable ? ( - - ) : ( - - )} - {!!error && {error}} - - - {unreachable && ( - - You need to authorize your proof on {serviceName}. - - )} - - {!Kb.Styles.isMobile && !unreachable && ( - - )} - {unreachable ? ( - - ) : ( - - )} - - - - - ) -} - -type InputProps = { - error: boolean - onChangeUsername: (arg0: string) => void - onEnterKeyDown: () => void - serviceIcon: T.Tracker.SiteIconSet - serviceSuffix: string - username: string -} - -const EnterUsernameInput = (props: InputProps) => { - const usernamePlaceholder = props.serviceSuffix === '@theqrl.org' ? 'Your QRL address' : 'Your username' - return ( - - - - {props.serviceSuffix} - - } - /> - - ) -} - -const Unreachable = (props: { - serviceIcon: T.Tracker.SiteIconSet - serviceSuffix: string - username: string -}) => ( - - - - - - {props.username} - - {props.serviceSuffix} - - - - - - - -) - -const styles = Kb.Styles.styleSheetCreate( - () => - ({ - backButton: { - left: Kb.Styles.globalMargins.small, - position: 'absolute', - top: Kb.Styles.globalMargins.small, - }, - buttonBar: { - ...Kb.Styles.padding( - Kb.Styles.globalMargins.small, - Kb.Styles.globalMargins.medium, - Kb.Styles.globalMargins.medium - ), - }, - buttonBarWarning: {backgroundColor: Kb.Styles.globalColors.yellow}, - buttonBig: {flex: 2.5}, - buttonSmall: {flex: 1}, - colorRed: {color: Kb.Styles.globalColors.redDark}, - container: {}, - - inlineIcon: { - position: 'relative', - top: 1, - }, - inputBox: { - ...Kb.Styles.padding(Kb.Styles.globalMargins.xsmall), - borderColor: Kb.Styles.globalColors.black_10, - borderRadius: Kb.Styles.borderRadius, - borderStyle: 'solid', - borderWidth: 1, - padding: Kb.Styles.globalMargins.xsmall, - }, - inputContainer: { - ...Kb.Styles.padding( - 0, - Kb.Styles.isMobile ? Kb.Styles.globalMargins.small : Kb.Styles.globalMargins.medium - ), - }, - marginLeftAuto: {marginLeft: 'auto'}, - opacity40: {opacity: 0.4}, - opacity75: {opacity: 0.75}, - placeholderService: {color: Kb.Styles.globalColors.black_20}, - serviceIconFull: { - height: 64, - width: 64, - }, - serviceIconHeaderContainer: {paddingTop: Kb.Styles.globalMargins.medium}, - serviceMeta: Kb.Styles.platformStyles({ - isElectron: { - paddingLeft: Kb.Styles.globalMargins.medium, - paddingRight: Kb.Styles.globalMargins.medium, - }, - isMobile: { - paddingLeft: Kb.Styles.globalMargins.small, - paddingRight: Kb.Styles.globalMargins.small, - }, - }), - serviceProofIcon: { - bottom: -Kb.Styles.globalMargins.tiny, - position: 'absolute', - right: -Kb.Styles.globalMargins.tiny, - }, - unreachableBox: Kb.Styles.platformStyles({ - common: {...Kb.Styles.padding(Kb.Styles.globalMargins.tiny, Kb.Styles.globalMargins.xsmall)}, - isElectron: {width: 360}, - }), - unreachablePlaceholder: Kb.Styles.platformStyles({ - common: {color: Kb.Styles.globalColors.black_35}, - isElectron: {wordBreak: 'break-all'}, - }), - warningText: {color: Kb.Styles.globalColors.brown_75, marginTop: Kb.Styles.globalMargins.small}, - }) as const -) - -export default ConnectedEnterUsername diff --git a/shared/profile/generic/result.tsx b/shared/profile/generic/result.tsx deleted file mode 100644 index c239da3bedb3..000000000000 --- a/shared/profile/generic/result.tsx +++ /dev/null @@ -1,89 +0,0 @@ -import * as C from '@/constants' -import * as Kb from '@/common-adapters' -import {SiteIcon} from './shared' -import {useCurrentUserState} from '@/stores/current-user' -import type {ProveGenericParams} from '@/stores/profile' -import {navToProfile} from '@/constants/router' - -type Props = { - error?: string - genericParams: ProveGenericParams - username: string -} - -const GenericResult = ({error = '', genericParams, username}: Props) => { - const proofUsername = username + genericParams.suffix - const serviceIcon = genericParams.logoFull - const currentUsername = useCurrentUserState(s => s.username) - const clearModals = C.useRouterState(s => s.dispatch.clearModals) - const onClose = () => { - clearModals() - navToProfile(currentUsername) - } - - const success = !error - const iconType = success ? 'icon-proof-success' : 'icon-proof-broken' - let frag = ( - <> - You are provably - {proofUsername} - - ) - if (!success) { - frag = ( - <> - {error} - - ) - } - return ( - - - - - - - - - {frag} - - - - - - ) -} - -const styles = Kb.Styles.styleSheetCreate( - () => - ({ - bottomContainer: { - height: 80, - }, - container: {}, - iconBadgeContainer: { - bottom: -5, - position: 'absolute', - right: -5, - }, - serviceIconContainer: Kb.Styles.platformStyles({ - common: { - marginBottom: Kb.Styles.globalMargins.tiny, - position: 'relative', - }, - }), - topContainer: Kb.Styles.platformStyles({ - common: { - flex: 1, - }, - }), - }) as const -) - -export default GenericResult diff --git a/shared/profile/post-proof.tsx b/shared/profile/post-proof.tsx deleted file mode 100644 index da0428c1b980..000000000000 --- a/shared/profile/post-proof.tsx +++ /dev/null @@ -1,253 +0,0 @@ -import * as C from '@/constants' -import {useProfileState} from '@/stores/profile' -import * as React from 'react' -import * as Kb from '@/common-adapters' -import {subtitle} from '@/util/platforms' -import {openURL as openUrl} from '@/util/misc' -import Modal from './modal' -import {useConfigState} from '@/stores/config' -import type * as T from '@/constants/types' - -type Props = { - error?: string - platform: T.More.PlatformsExpandedType - proofText: string - username: string -} - -const Container = ({error, platform, username, proofText: initialProofText}: Props) => { - let proofText = initialProofText - const cancelAddProof = useProfileState(s => s.dispatch.dynamic.cancelAddProof) - const afterCheckProof = useProfileState(s => s.dispatch.dynamic.afterCheckProof) - if ( - platform === 'zcash' || - platform === 'btc' || - platform === 'dnsOrGenericWebSite' || - platform === 'pgp' - ) { - throw new Error(`Invalid profile platform in PostProofContainer: ${platform}`) - } - - let url = '' - let openLinkBeforeSubmit = false - switch (platform) { - case 'twitter': - openLinkBeforeSubmit = true - url = proofText ? `https://twitter.com/home?status=${proofText}` : '' - break - case 'github': - openLinkBeforeSubmit = true - url = 'https://gist.github.com/' - break - case 'reddit': // fallthrough - case 'facebook': - openLinkBeforeSubmit = true - url = proofText ? proofText : '' - proofText = '' - break - case 'hackernews': - openLinkBeforeSubmit = true - url = `https://news.ycombinator.com/user?id=${username}` - break - default: - break - } - const platformUserName = username - const copyToClipboard = useConfigState(s => s.dispatch.defer.copyToClipboard) - const clearModals = C.useRouterState(s => s.dispatch.clearModals) - const onCancel = () => { - clearModals() - cancelAddProof?.() - } - const onSubmit = () => afterCheckProof?.() - const errorMessage = error ?? '' - const [showSubmit, setShowSubmit] = React.useState(!openLinkBeforeSubmit) - const platformSubtitle = subtitle(platform) - const proofActionText = actionMap.get(platform) ?? '' - const onCompleteText = checkMap.get(platform) ?? 'OK posted! Check for it!' - const noteText = noteMap.get(platform) ?? '' - const DescriptionView = descriptionMap[platform] - return ( - - - { - e.preventDefault() - proofText && copyToClipboard(proofText) - }} - > - {!!errorMessage && ( - - - {errorMessage} - - - )} - - <> - - {platformUserName} - - {!!platformSubtitle && ( - - {platformSubtitle} - - )} - - - {!!proofText && } - {!!noteText && ( - - {noteText} - - )} - - - {showSubmit ? ( - - ) : ( - { - setShowSubmit(true) - url && openUrl(url) - }} - label={proofActionText || ''} - /> - )} - - - - - ) -} - -const actionMap = new Map([ - ['github', 'Create gist now'], - ['hackernews', 'Go to Hacker News'], - ['reddit', 'Reddit form'], - ['twitter', 'Tweet it now'], -]) - -const checkMap = new Map([['twitter', 'OK tweeted! Check for it!']]) - -const webNote = 'Note: If someone already verified this domain, just append to the existing keybase.txt file.' - -const noteMap = new Map([ - ['http', webNote], - ['https', webNote], - ['reddit', "Make sure you're signed in to Reddit, and don't edit the text or title before submitting."], - ['web', webNote], -]) - -const WebDescription = ({platformUserName}: {platformUserName: string}) => { - const root = `${platformUserName}/keybase.txt` - const wellKnown = `${platformUserName}/.well-known/keybase.txt` - const rootUrlProps = Kb.useClickURL(`https://${root}`) - const wellKnownUrlProps = Kb.useClickURL(`https://${wellKnown}`) - return ( - - - Please serve the text below exactly as it appears - {" at one of these URL's."} - - - {root} - - - {wellKnown} - - - ) -} - -const descriptionMap = { - dns: () => ( - - Enter the following as a TXT entry in your DNS zone,{' '} - exactly as it appears - {'. If you need a "name" for your entry, give it "@".'} - - ), - facebook: () => null, - github: () => ( - - Login to GitHub and paste the text below into a public gist - called keybase.md. - - ), - hackernews: () => ( - - Please add the below text{' '} - - exactly as it appears - {' '} - to your profile. - - ), - http: WebDescription, - https: WebDescription, - reddit: () => ( - - Click the button below and post the form in the subreddit{' '} - KeybaseProofs. - - ), - rooter: () => null, - twitter: () => ( - - Please tweet the text below{' '} - - exactly as it appears. - - - ), - web: WebDescription, -} as const - -const styles = Kb.Styles.styleSheetCreate(() => ({ - blue: {color: Kb.Styles.globalColors.blueDark}, - center: {alignSelf: 'center'}, - error: { - alignSelf: 'center', - backgroundColor: Kb.Styles.globalColors.red, - borderRadius: Kb.Styles.borderRadius, - padding: Kb.Styles.globalMargins.medium, - }, - grey: {color: Kb.Styles.globalColors.black_20}, - proof: { - flexGrow: 1, - minHeight: 116, - }, - scroll: {width: '100%'}, - scrollContent: {width: '100%'}, -})) - -export default Container diff --git a/shared/profile/prove-enter-username.tsx b/shared/profile/prove-enter-username.tsx deleted file mode 100644 index 6c87d1f98dfd..000000000000 --- a/shared/profile/prove-enter-username.tsx +++ /dev/null @@ -1,223 +0,0 @@ -import * as C from '@/constants' -import {useProfileState} from '@/stores/profile' -import * as React from 'react' -import * as Kb from '@/common-adapters' -import Modal from './modal' -import * as T from '@/constants/types' -import {normalizeProofUsername} from './proof-utils' - -type Props = { - error?: string - platform: T.More.PlatformsExpandedType - username?: string -} - -const Container = ({error: routeError, platform, username: initialUsername = ''}: Props) => { - const cancelAddProof = useProfileState(s => s.dispatch.dynamic.cancelAddProof) - const submitUsername = useProfileState(s => s.dispatch.dynamic.submitUsername) - const registerCryptoAddress = C.useRPC(T.RPCGen.cryptocurrencyRegisterAddressRpcPromise) - const [username, setUsername] = React.useState(initialUsername) - const [errorText, setErrorText] = React.useState(routeError === 'Input canceled' ? '' : (routeError ?? '')) - const [canSubmit, setCanSubmit] = React.useState(!!initialUsername.length) - - React.useEffect(() => { - setErrorText(routeError === 'Input canceled' ? '' : (routeError ?? '')) - }, [routeError]) - - React.useEffect(() => { - setUsername(initialUsername) - setCanSubmit(!!initialUsername.length) - }, [initialUsername]) - - const navigateAppend = C.useRouterState(s => s.dispatch.navigateAppend) - const _onSubmit = (input: string, platform?: string) => { - const {normalized, valid} = normalizeProofUsername( - platform as T.More.PlatformsExpandedType | undefined, - input - ) - - if (platform === 'btc') { - if (!valid) { - setErrorText('Invalid address format') - return - } - setErrorText('') - registerCryptoAddress( - [{address: normalized, force: true, wantedFamily: 'bitcoin'}, C.waitingKeyProfile], - () => { - navigateAppend({ - name: 'profileConfirmOrPending', - params: { - platform, - proofFound: true, - proofStatus: T.RPCGen.ProofStatus.ok, - username: normalized, - }, - }) - }, - error => { - setErrorText(error.desc) - } - ) - } else if (platform === 'zcash') { - setErrorText('') - registerCryptoAddress( - [{address: normalized, force: true, wantedFamily: 'zcash'}, C.waitingKeyProfile], - () => { - navigateAppend({ - name: 'profileConfirmOrPending', - params: { - platform, - proofFound: true, - proofStatus: T.RPCGen.ProofStatus.ok, - username: normalized, - }, - }) - }, - error => { - setErrorText(error.desc) - } - ) - } else { - setErrorText('') - submitUsername?.(normalized) - } - } - const clearModals = C.useRouterState(s => s.dispatch.clearModals) - const onCancel = () => { - cancelAddProof?.() - clearModals() - } - const onSubmit = (username: string) => _onSubmit(username, platform) - - const submit = () => { - if (canSubmit) { - onSubmit(username) - } - } - - const onChangeUsername = (username: string) => { - setUsername(username) - setCanSubmit(!!username.length) - } - - const pt = platformText[platform] - if (!pt.headerText) { - throw new Error(`Proofs for platform ${platform} are unsupported.`) - } - const {headerText, hintText} = pt - - return ( - - {!!errorText && ( - - - {errorText} - - - )} - - {C.isMobile ? null : ( - - {headerText} - - )} - - - - - - - - - - ) -} - -const UsernameTips = ({platform}: {platform: T.More.PlatformsExpandedType}) => - platform === 'hackernews' ? ( - - • You must have karma ≥ 2 - • You must enter your uSeRName with exact case - - ) : null - -const standardText = (name: string) => ({ - headerText: C.isMobile ? `Prove ${name}` : `Prove your ${name} identity`, - hintText: `Your ${name} username`, -}) - -const invalidText = () => ({ - headerText: '', - hintText: '', -}) - -const platformText = { - btc: { - headerText: 'Set a Bitcoin address', - hintText: 'Your Bitcoin address', - }, - dns: { - headerText: 'Prove your domain', - hintText: 'yourdomain.com', - }, - dnsOrGenericWebSite: invalidText(), - facebook: standardText('Facebook'), - github: standardText('GitHub'), - hackernews: standardText('Hacker News'), - http: { - headerText: 'Prove your http website', - hintText: 'http://whatever.yoursite.com', - }, - https: { - headerText: 'Prove your https website', - hintText: 'https://whatever.yoursite.com', - }, - pgp: invalidText(), - reddit: standardText('Reddit'), - rooter: invalidText(), - twitter: standardText('Twitter'), - web: { - headerText: 'Prove your website', - hintText: 'whatever.yoursite.com', - }, - zcash: { - headerText: 'Set a Zcash address', - hintText: 'Your z_address or t_address', - }, -} - -const styles = Kb.Styles.styleSheetCreate(() => ({ - centered: {alignSelf: 'center'}, - error: { - backgroundColor: Kb.Styles.globalColors.red, - borderRadius: Kb.Styles.borderRadius, - marginBottom: Kb.Styles.globalMargins.small, - padding: Kb.Styles.globalMargins.medium, - }, - tips: {padding: Kb.Styles.globalMargins.small}, -})) - -export default Container diff --git a/shared/profile/routes.tsx b/shared/profile/routes.tsx index c4c1d21128bd..49a0027644a9 100644 --- a/shared/profile/routes.tsx +++ b/shared/profile/routes.tsx @@ -82,9 +82,6 @@ export const newModalRoutes = { }, } ), - profileConfirmOrPending: C.makeScreen(React.lazy(async () => import('./confirm-or-pending')), { - getOptions: {modalStyle: profileModalStyle}, - }), profileEdit: C.makeScreen(React.lazy(async () => import('./edit-profile')), { getOptions: {modalStyle: {height: 450, width: 350}, title: 'Edit Profile'}, }), @@ -97,23 +94,11 @@ export const newModalRoutes = { }), profileFinished: C.makeScreen(React.lazy(async () => import('./pgp/finished'))), profileGenerate: C.makeScreen(React.lazy(async () => import('./pgp/generate'))), - profileGenericEnterUsername: C.makeScreen(React.lazy(async () => import('./generic/enter-username')), { - getOptions: {gestureEnabled: false, modalStyle: {height: 485, width: 560}}, - }), - profileGenericProofResult: C.makeScreen(React.lazy(async () => import('./generic/result')), { - getOptions: {modalStyle: {height: 485, width: 560}}, - }), profileImport: C.makeScreen(React.lazy(async () => import('./pgp/import'))), profilePgp: C.makeScreen(React.lazy(async () => import('./pgp/choice'))), - profilePostProof: C.makeScreen(React.lazy(async () => import('./post-proof')), { - getOptions: {modalStyle: profileModalStyle}, - }), profileProofsList: C.makeScreen(React.lazy(async () => import('./generic/proofs-list')), { getOptions: {modalStyle: {height: 485, width: 560}, title: 'Prove your...'}, }), - profileProveEnterUsername: C.makeScreen(React.lazy(async () => import('./prove-enter-username')), { - getOptions: {modalStyle: profileModalStyle}, - }), profileProveWebsiteChoice: C.makeScreen(React.lazy(async () => import('./prove-website-choice')), { getOptions: {modalStyle: profileModalStyle}, }), diff --git a/shared/profile/user/hooks.tsx b/shared/profile/user/hooks.tsx index 7ce3ead47860..4e7ae74f9d1f 100644 --- a/shared/profile/user/hooks.tsx +++ b/shared/profile/user/hooks.tsx @@ -3,9 +3,9 @@ import type * as T from '@/constants/types' import {type BackgroundColorType} from '.' import {useColorScheme} from 'react-native' import {useTrackerState} from '@/stores/tracker' -import {useProfileState} from '@/stores/profile' import {useFollowerState} from '@/stores/followers' import {useCurrentUserState} from '@/stores/current-user' +import {editAvatar} from '@/util/misc' const headerBackgroundColorType = ( state: T.Tracker.DetailsState, @@ -126,7 +126,6 @@ const useUserData = (username: string) => { } })() - const editAvatar = useProfileState(s => s.dispatch.editAvatar) const _onEditAvatar = editAvatar // const _onIKnowThem = (username: string, guiID: string) => { // dispatch( diff --git a/shared/stores/profile.tsx b/shared/stores/profile.tsx index bb1b45d20a50..6d89fa430c3d 100644 --- a/shared/stores/profile.tsx +++ b/shared/stores/profile.tsx @@ -3,35 +3,6 @@ import {ignorePromise, wrapErrors} from '@/constants/utils' import * as Z from '@/util/zustand' import {RPCError} from '@/util/errors' import {navigateAppend} from '@/constants/router' -import type {useTrackerState} from '@/stores/tracker' - -export type ProveGenericParams = { - logoBlack: T.Tracker.SiteIconSet - logoFull: T.Tracker.SiteIconSet - title: string - subtext: string - suffix: string - buttonLabel: string -} - -export const makeProveGenericParams = (): ProveGenericParams => ({ - buttonLabel: '', - logoBlack: [], - logoFull: [], - subtext: '', - suffix: '', - title: '', -}) - -export const toProveGenericParams = (p: T.RPCGen.ProveParameters): T.Immutable => ({ - ...makeProveGenericParams(), - buttonLabel: p.buttonLabel, - logoBlack: p.logoBlack || [], - logoFull: p.logoFull || [], - subtext: p.subtext, - suffix: p.suffix, - title: p.title, -}) type GeneratePgpArgs = { pgpEmail1: string @@ -40,46 +11,18 @@ type GeneratePgpArgs = { pgpFullName: string } -type Store = T.Immutable<{ -}> - -const initialStore: Store = {} - -export type State = Store & { +export type State = { dispatch: { - defer: { - onTracker2GetDetails?: (username: string) => T.Tracker.Details | undefined - onTracker2Load?: ( - params: Parameters['dispatch']['load']>[0] - ) => void - onTracker2ShowUser?: (username: string, asTracker: boolean, skipNav?: boolean) => void - onTracker2UpdateResult?: (guiID: string, result: T.Tracker.DetailsState, reason?: string) => void - } dynamic: { - afterCheckProof?: () => void - cancelAddProof?: () => void cancelPgpGen?: () => void finishedWithKeyGen?: (shouldStoreKeyOnServer: boolean) => void - submitUsername?: (username: string) => void } - editAvatar: () => void generatePgp: (args: GeneratePgpArgs) => void resetState: () => void } } -export const useProfileState = Z.createZustand('profile', (set, get) => { - const resetProofCallbacks = () => { - set(s => { - s.dispatch.dynamic.afterCheckProof = undefined - s.dispatch.dynamic.cancelAddProof = defaultCancelAddProof - s.dispatch.dynamic.submitUsername = undefined - }) - } - const defaultCancelAddProof = () => { - resetProofCallbacks() - } - +export const useProfileState = Z.createZustand('profile', set => { const resetPgpCallbacks = () => { set(s => { s.dispatch.dynamic.cancelPgpGen = undefined @@ -88,28 +31,9 @@ export const useProfileState = Z.createZustand('profile', (set, get) => { } const dispatch: State['dispatch'] = { - defer: { - onTracker2GetDetails: () => { - throw new Error('onTracker2GetDetails not implemented') - }, - onTracker2Load: () => { - throw new Error('onTracker2Load not implemented') - }, - onTracker2ShowUser: () => { - throw new Error('onTracker2ShowUser not implemented') - }, - onTracker2UpdateResult: () => { - throw new Error('onTracker2UpdateResult not implemented') - }, - }, dynamic: { - cancelAddProof: defaultCancelAddProof, cancelPgpGen: undefined, finishedWithKeyGen: undefined, - submitUsername: undefined, - }, - editAvatar: () => { - throw new Error('This is overloaded by platform specific') }, generatePgp: args => { const f = async () => { @@ -173,15 +97,11 @@ export const useProfileState = Z.createZustand('profile', (set, get) => { resetState: () => { set(s => ({ ...s, - ...initialStore, dispatch: { ...s.dispatch, dynamic: { - afterCheckProof: undefined, - cancelAddProof: defaultCancelAddProof, cancelPgpGen: undefined, finishedWithKeyGen: undefined, - submitUsername: undefined, }, }, })) @@ -189,7 +109,6 @@ export const useProfileState = Z.createZustand('profile', (set, get) => { } return { - ...initialStore, dispatch, } }) diff --git a/shared/stores/tests/profile.test.ts b/shared/stores/tests/profile.test.ts index a74e4d091521..a5c12d745e13 100644 --- a/shared/stores/tests/profile.test.ts +++ b/shared/stores/tests/profile.test.ts @@ -47,20 +47,15 @@ test('validatePgpInfo derives validation errors from local form state', () => { expect(result.pgpErrorText).toBe('') }) -test('resetState clears dynamic profile callbacks and preserves the default cancel hook', () => { +test('resetState clears dynamic pgp callbacks', () => { useProfileState.setState(s => { - s.dispatch.dynamic.afterCheckProof = () => {} s.dispatch.dynamic.cancelPgpGen = () => {} s.dispatch.dynamic.finishedWithKeyGen = () => {} - s.dispatch.dynamic.submitUsername = () => {} }) useProfileState.getState().dispatch.resetState() const state = useProfileState.getState() - expect(state.dispatch.dynamic.afterCheckProof).toBeUndefined() expect(state.dispatch.dynamic.cancelPgpGen).toBeUndefined() expect(state.dispatch.dynamic.finishedWithKeyGen).toBeUndefined() - expect(state.dispatch.dynamic.submitUsername).toBeUndefined() - expect(typeof state.dispatch.dynamic.cancelAddProof).toBe('function') }) diff --git a/shared/util/misc.d.ts b/shared/util/misc.d.ts index cb76bc7ea9ac..7ff59d3b09f3 100644 --- a/shared/util/misc.d.ts +++ b/shared/util/misc.d.ts @@ -5,6 +5,7 @@ type NotifyPopupOpts = {body?: string; sound?: boolean} export declare function openURL(url?: string): void export declare function openSMS(phonenos: Array, body?: string): Promise export declare function clearLocalLogs(): Promise +export declare function editAvatar(): void export declare function pickImages(title: string): Promise> export declare function pickFiles(options: OpenDialogOptions): Promise> export declare function pickSave(options: SaveDialogOptions): Promise diff --git a/shared/util/misc.desktop.tsx b/shared/util/misc.desktop.tsx index 957fd706264e..daca3122e9f6 100644 --- a/shared/util/misc.desktop.tsx +++ b/shared/util/misc.desktop.tsx @@ -1,4 +1,5 @@ import logger from '@/logger' +import {navigateAppend} from '@/constants/router' import debounce from 'lodash/debounce' import KB2, {type OpenDialogOptions, type SaveDialogOptions} from './electron.desktop' @@ -34,6 +35,10 @@ export const clearLocalLogs = async (): Promise => { // noop on desktop } +export const editAvatar = () => { + navigateAppend({name: 'profileEditAvatar', params: {image: undefined}}) +} + export const pickImages = async (title: string) => { if (!showOpenDialog) return [] const filePaths = await showOpenDialog({ diff --git a/shared/util/misc.native.tsx b/shared/util/misc.native.tsx index d27544e6a9f8..fd44ccfa3f54 100644 --- a/shared/util/misc.native.tsx +++ b/shared/util/misc.native.tsx @@ -1,6 +1,8 @@ +import {navigateAppend} from '@/constants/router' +import {useConfigState} from '@/stores/config' import {isIOS} from '@/constants/platform.native' import {pickDocumentsAsync} from './expo-document-picker.native' -import {launchImageLibraryAsync} from './expo-image-picker.native' +import {launchImageLibraryAsync, type ImageInfo} from './expo-image-picker.native' import type {OpenDialogOptions, SaveDialogOptions} from './electron.desktop' import * as SMS from 'expo-sms' import {Linking} from 'react-native' @@ -30,6 +32,26 @@ export const clearLocalLogs = async (): Promise => { return clearLocalLogsNative() } +export const editAvatar = () => { + const f = async () => { + try { + const result = await launchImageLibraryAsync('photo') + const first = result.assets?.reduce((acc, a) => { + if (!acc && (a.type === 'image' || a.type === 'video')) { + return a as ImageInfo + } + return acc + }, undefined) + if (!result.canceled && first) { + navigateAppend({name: 'profileEditAvatar', params: {image: first}}) + } + } catch (error) { + useConfigState.getState().dispatch.filePickerError(new Error(String(error))) + } + } + void f() +} + export const pickImages = async (_: string): Promise> => { const result = await launchImageLibraryAsync('photo') return result.canceled ? [] : result.assets.map(a => a.uri) From 9d55b127b4216050c7ff435b4d3a205b4b3b2036 Mon Sep 17 00:00:00 2001 From: Chris Nojima Date: Fri, 27 Mar 2026 17:16:00 -0400 Subject: [PATCH 12/14] WIP --- AGENTS.md | 1 + shared/constants/init/index.native.tsx | 7 ++++--- shared/profile/generic/proofs-list.tsx | 27 +++++++++++++++++++------- 3 files changed, 25 insertions(+), 10 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index fd283654fdb1..8a29cb615d48 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,5 +1,6 @@ # Repo Notes - This repo uses React Compiler. Assume React Compiler patterns are enabled when editing React code, and avoid adding `useMemo`/`useCallback` by default unless they are clearly needed for correctness or compatibility with existing code. +- Treat React mount/unmount effects as Strict-Mode-safe. Do not assume a component only mounts once; route-driven async startup and cleanup logic must be idempotent and must not leave refs or guards stuck false after a dev remount. - When a component reads multiple adjacent values from the same store hook, prefer a consolidated selector with `C.useShallow(...)` instead of multiple separate subscriptions. - Do not add new exported functions, types, or constants unless they are required outside the file. Prefer file-local helpers for one-off implementation details and tests. diff --git a/shared/constants/init/index.native.tsx b/shared/constants/init/index.native.tsx index a7359075146f..4a172dd7468a 100644 --- a/shared/constants/init/index.native.tsx +++ b/shared/constants/init/index.native.tsx @@ -21,6 +21,7 @@ import {Alert, Linking} from 'react-native' import {isAndroid} from '@/constants/platform.native' import {wrapErrors} from '@/util/debug' import {getTab, getVisiblePath, logState, switchTab} from '@/constants/router' +import {launchImageLibraryAsync} from '@/util/expo-image-picker.native' import {pickDocumentsAsync} from '@/util/expo-document-picker.native' import {setupAudioMode} from '@/util/audio.native' import { @@ -525,9 +526,9 @@ export const initPlatformListener = () => { try { const result = await launchImageLibraryAsync(type, true, true) if (result.canceled) return - result.assets.map(r => - useFSState.getState().dispatch.upload(parentPath, Styles.unnormalizePath(r.uri)) - ) + for (const asset of result.assets) { + useFSState.getState().dispatch.upload(parentPath, Styles.unnormalizePath(asset.uri)) + } } catch (e) { errorToActionOrThrow(e) } diff --git a/shared/profile/generic/proofs-list.tsx b/shared/profile/generic/proofs-list.tsx index c4d1bb8925d4..705f0a667594 100644 --- a/shared/profile/generic/proofs-list.tsx +++ b/shared/profile/generic/proofs-list.tsx @@ -130,6 +130,7 @@ const Container = ({platform, reason = 'profile'}: Props) => { })) const mountedRef = React.useRef(true) + const initialProofStartedRef = React.useRef(false) const initialRouteRef = React.useRef({platform, reason}) const currentUsernameRef = React.useRef('') const currentGenericParamsRef = React.useRef(makeProveGenericParams()) @@ -160,9 +161,16 @@ const Container = ({platform, reason = 'profile'}: Props) => { } React.useEffect(() => { + mountedRef.current = true return () => { mountedRef.current = false - cancelSession() + const cancel = cancelCurrentRef.current + afterCheckProofRef.current = undefined + cancelCurrentRef.current = undefined + submitUsernameRef.current = undefined + currentGenericParamsRef.current = makeProveGenericParams() + currentUsernameRef.current = '' + cancel?.() } }, []) @@ -307,7 +315,7 @@ const Container = ({platform, reason = 'profile'}: Props) => { username: currentUsernameRef.current, }) openUrl(proof) - afterCheckProofRef.current?.() + afterCheckProofRef.current() } }, 'keybase.1.proveUi.preProofWarning': (_, response) => response.result(true), @@ -467,7 +475,8 @@ const Container = ({platform, reason = 'profile'}: Props) => { React.useEffect(() => { const {platform: initialPlatform, reason: initialReason} = initialRouteRef.current - if (initialPlatform) { + if (initialPlatform && !initialProofStartedRef.current) { + initialProofStartedRef.current = true startProofRef.current(initialPlatform, initialReason) } }, []) @@ -900,7 +909,7 @@ const PostProof = ({ const proofActionText = actionMap.get(step.platform) ?? '' const onCompleteText = checkMap.get(step.platform) ?? 'OK posted! Check for it!' const noteText = noteMap.get(step.platform) ?? '' - const DescriptionView = descriptionMap[step.platform] + const DescriptionView = descriptionMap[step.platform] ?? EmptyDescription return ( @@ -1196,7 +1205,11 @@ const WebDescription = ({platformUserName}: {platformUserName: string}) => { ) } -const descriptionMap = { +const EmptyDescription = () => null + +const descriptionMap: Partial< + Record> +> = { dns: () => ( Enter the following as a TXT entry in your DNS zone, exactly as it appears @@ -1230,7 +1243,7 @@ const descriptionMap = { ), web: WebDescription, -} satisfies Record> +} const messageMap = new Map([ ['btc', 'Your Bitcoin address has now been signed onto your profile.'], @@ -1277,8 +1290,8 @@ const styles = Kb.Styles.styleSheetCreate( buttonBarWarning: {backgroundColor: Kb.Styles.globalColors.yellow}, buttonBig: {flex: 2.5}, buttonSmall: {flex: 1}, - centered: {alignSelf: 'center'}, center: {alignSelf: 'center'}, + centered: {alignSelf: 'center'}, colorRed: {color: Kb.Styles.globalColors.redDark}, container: Kb.Styles.platformStyles({ common: {flex: 1}, From 37f54c7508c971b06fdefea6be586d5404486f88 Mon Sep 17 00:00:00 2001 From: Chris Nojima Date: Fri, 27 Mar 2026 17:32:21 -0400 Subject: [PATCH 13/14] WIP --- shared/profile/pgp/choice/index.desktop.tsx | 333 ++++++++++++++++-- shared/profile/pgp/finished/index.d.ts | 3 - shared/profile/pgp/finished/index.desktop.tsx | 100 ------ shared/profile/pgp/finished/index.native.tsx | 1 - shared/profile/pgp/generate/index.d.ts | 3 - shared/profile/pgp/generate/index.desktop.tsx | 27 -- shared/profile/pgp/generate/index.native.tsx | 1 - shared/profile/pgp/info/index.d.ts | 3 - shared/profile/pgp/info/index.desktop.tsx | 85 ----- shared/profile/pgp/info/index.native.tsx | 1 - shared/profile/routes.tsx | 3 - shared/stores/profile.tsx | 114 ------ shared/stores/store-registry.tsx | 8 - shared/stores/tests/profile.test.ts | 14 - 14 files changed, 310 insertions(+), 386 deletions(-) delete mode 100644 shared/profile/pgp/finished/index.d.ts delete mode 100644 shared/profile/pgp/finished/index.desktop.tsx delete mode 100644 shared/profile/pgp/finished/index.native.tsx delete mode 100644 shared/profile/pgp/generate/index.d.ts delete mode 100644 shared/profile/pgp/generate/index.desktop.tsx delete mode 100644 shared/profile/pgp/generate/index.native.tsx delete mode 100644 shared/profile/pgp/info/index.d.ts delete mode 100644 shared/profile/pgp/info/index.desktop.tsx delete mode 100644 shared/profile/pgp/info/index.native.tsx delete mode 100644 shared/stores/profile.tsx diff --git a/shared/profile/pgp/choice/index.desktop.tsx b/shared/profile/pgp/choice/index.desktop.tsx index 5d15f6012d34..4c93850dbe86 100644 --- a/shared/profile/pgp/choice/index.desktop.tsx +++ b/shared/profile/pgp/choice/index.desktop.tsx @@ -1,40 +1,327 @@ import * as Kb from '@/common-adapters' import * as C from '@/constants' +import * as React from 'react' +import * as T from '@/constants/types' +import {ignorePromise} from '@/constants/utils' +import {RPCError} from '@/util/errors' import Modal from '@/profile/modal' +import {validatePgpInfo} from '../validation' + +type GeneratePgpArgs = { + pgpEmail1: string + pgpEmail2: string + pgpEmail3: string + pgpFullName: string +} + +type Step = + | {kind: 'choice'} + | {kind: 'info'} + | {kind: 'generate'} + | {kind: 'finished'; pgpKeyString: string; promptShouldStoreKeyOnServer: boolean} + +const makeInitialForm = (): GeneratePgpArgs => ({ + pgpEmail1: '', + pgpEmail2: '', + pgpEmail3: '', + pgpFullName: '', +}) export default function Choice() { - const clearModals = C.useRouterState(s => s.dispatch.clearModals) + const {clearModals, navigateAppend} = C.useRouterState( + C.useShallow(s => ({ + clearModals: s.dispatch.clearModals, + navigateAppend: s.dispatch.navigateAppend, + })) + ) + const mountedRef = React.useRef(true) + const cancelCurrentRef = React.useRef void)>(undefined) + const finishCurrentRef = React.useRef void)>(undefined) + const [form, setForm] = React.useState(makeInitialForm) + const [step, setStep] = React.useState({kind: 'choice'}) + + React.useEffect(() => { + mountedRef.current = true + return () => { + mountedRef.current = false + const cancel = cancelCurrentRef.current + cancelCurrentRef.current = undefined + finishCurrentRef.current = undefined + cancel?.() + } + }, []) + + const setStepSafe = (next: Step) => { + if (mountedRef.current) { + setStep(next) + } + } + const onCancel = () => { + if (step.kind === 'info') { + setStepSafe({kind: 'choice'}) + return + } + if (step.kind === 'generate') { + cancelCurrentRef.current?.() + } clearModals() } - const navigateAppend = C.useRouterState(s => s.dispatch.navigateAppend) + const onShowGetNew = () => { - navigateAppend('profileProvideInfo') + setStepSafe({kind: 'info'}) } const onShowImport = () => { navigateAppend('profileImport') } - return ( - - - Add a PGP key - ) => { + setForm(s => ({...s, ...next})) + } + + const data = {...form, ...validatePgpInfo(form)} + const nextDisabled = !data.pgpEmail1 || !data.pgpFullName || !!data.pgpErrorText + + const onGenerate = () => { + if (nextDisabled) { + return + } + const args = form + setStepSafe({kind: 'generate'}) + + ignorePromise( + (async () => { + let canceled = false + let pgpKeyString = 'Error getting public key...' + const inputCancelError = {code: T.RPCGen.StatusCode.scinputcanceled, desc: 'Input canceled'} + const ids = [args.pgpEmail1, args.pgpEmail2, args.pgpEmail3].filter(Boolean).map(email => ({ + comment: '', + email, + username: args.pgpFullName, + })) + + cancelCurrentRef.current = () => { + canceled = true + } + + try { + await T.RPCGen.pgpPgpKeyGenDefaultRpcListener({ + customResponseIncomingCallMap: { + 'keybase.1.pgpUi.keyGenerated': ({key}, response) => { + if (canceled || !mountedRef.current) { + response.error(inputCancelError) + return + } + pgpKeyString = key.key + response.result() + }, + 'keybase.1.pgpUi.shouldPushPrivate': ({prompt}, response) => { + if (canceled || !mountedRef.current) { + response.error(inputCancelError) + return + } + cancelCurrentRef.current = () => { + canceled = true + response.error(inputCancelError) + } + finishCurrentRef.current = (shouldStoreKeyOnServer: boolean) => { + finishCurrentRef.current = undefined + response.result(shouldStoreKeyOnServer) + } + setStepSafe({kind: 'finished', pgpKeyString, promptShouldStoreKeyOnServer: prompt}) + }, }, - ]} - /> - + incomingCallMap: {'keybase.1.pgpUi.finished': () => {}}, + params: {createUids: {ids, useDefault: false}}, + }) + } catch (error) { + if (!(error instanceof RPCError)) { + return + } + if (error.code !== T.RPCGen.StatusCode.scinputcanceled) { + throw error + } + } finally { + cancelCurrentRef.current = undefined + finishCurrentRef.current = undefined + } + })() + ) + } + + const content = (() => { + switch (step.kind) { + case 'choice': + return ( + + Add a PGP key + + + ) + case 'info': + return ( + <> + + + + Fill in your public info. + + onUpdate({pgpFullName})} + /> + onUpdate({pgpEmail1})} + onEnterKeyDown={onGenerate} + value={data.pgpEmail1} + error={data.pgpErrorEmail1} + /> + onUpdate({pgpEmail2})} + onEnterKeyDown={onGenerate} + value={data.pgpEmail2} + error={data.pgpErrorEmail2} + /> + onUpdate({pgpEmail3})} + onEnterKeyDown={onGenerate} + value={data.pgpEmail3} + error={data.pgpErrorEmail3} + /> + + {data.pgpErrorText || 'Include any addresses you plan to use for PGP encrypted email.'} + + + + + + + + ) + case 'generate': + return ( + + + Generating your unique key... + + Math time! You are about to discover a 4096-bit key pair. +
+ This could take as long as a couple of minutes. +
+ +
+ ) + case 'finished': + return ( + { + const finish = finishCurrentRef.current + finishCurrentRef.current = undefined + finish?.(shouldStoreKeyOnServer) + clearModals() + }} + pgpKeyString={step.pgpKeyString} + promptShouldStoreKeyOnServer={step.promptShouldStoreKeyOnServer} + /> + ) + } + })() + + const skipButton = step.kind === 'info' || step.kind === 'finished' + return ( + + {content} ) } + +const Finished = (props: { + onDone: (shouldStoreKeyOnServer: boolean) => void + promptShouldStoreKeyOnServer: boolean + pgpKeyString: string +}) => { + const [shouldStoreKeyOnServer, setShouldStoreKeyOnServer] = React.useState(false) + + return ( + + + Here is your unique public key! + + Your private key has been written to Keybase’s local keychain. You can learn to use it with `keybase + pgp help` from your terminal. If you have GPG installed, it has also been written to GPG’s keychain. + +