diff --git a/assets/images/germ_logo.webp b/assets/images/germ_logo.webp
new file mode 100644
index 0000000000..a85048381a
Binary files /dev/null and b/assets/images/germ_logo.webp differ
diff --git a/src/components/dialogs/LinkWarning.tsx b/src/components/dialogs/LinkWarning.tsx
index 74be78f9d3..aeb3e959ce 100644
--- a/src/components/dialogs/LinkWarning.tsx
+++ b/src/components/dialogs/LinkWarning.tsx
@@ -22,12 +22,12 @@ export function LinkWarningDialog() {
webOptions={{alignCenter: true}}
onClose={linkWarningDialogControl.clear}>
-
+
)
}
-function InAppBrowserConsentInner({
+function LinkWarningDialogInner({
link,
}: {
link?: {href: string; displayText: string; share?: boolean}
diff --git a/src/screens/Profile/Header/ProfileHeaderStandard.tsx b/src/screens/Profile/Header/ProfileHeaderStandard.tsx
index 6467756d4e..c60fcd032a 100644
--- a/src/screens/Profile/Header/ProfileHeaderStandard.tsx
+++ b/src/screens/Profile/Header/ProfileHeaderStandard.tsx
@@ -39,6 +39,7 @@ import * as Toast from '#/components/Toast'
import {Text} from '#/components/Typography'
import {VerificationCheckButton} from '#/components/verification/VerificationCheckButton'
import {IS_IOS} from '#/env'
+import {GermButton} from '../components/GermButton'
import {EditProfileDialog} from './EditProfileDialog'
import {ProfileHeaderHandle} from './Handle'
import {ProfileHeaderMetrics} from './Metrics'
@@ -163,6 +164,10 @@ let ProfileHeaderStandard = ({
) : undefined}
+ {profile.associated?.germ && (
+
+ )}
+
{!isMe &&
!isBlockedUser &&
shouldShowKnownFollowers(profile.viewer?.knownFollowers) && (
diff --git a/src/screens/Profile/components/GermButton.tsx b/src/screens/Profile/components/GermButton.tsx
new file mode 100644
index 0000000000..70e02d1f7d
--- /dev/null
+++ b/src/screens/Profile/components/GermButton.tsx
@@ -0,0 +1,221 @@
+import {Platform, View} from 'react-native'
+import {Image} from 'expo-image'
+import {type AppBskyActorDefs} from '@atproto/api'
+import {msg, Trans} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+import {useMutation} from '@tanstack/react-query'
+
+import {isNetworkError} from '#/lib/strings/errors'
+import {logger} from '#/logger'
+import {useAgent, useSession} from '#/state/session'
+import {atoms as a, useTheme, web} from '#/alf'
+import {Button, ButtonIcon, ButtonText} from '#/components/Button'
+import * as Dialog from '#/components/Dialog'
+import {ArrowTopRight_Stroke2_Corner0_Rounded as ArrowTopRightIcon} from '#/components/icons/Arrow'
+import {Link} from '#/components/Link'
+import {Loader} from '#/components/Loader'
+import * as Toast from '#/components/Toast'
+import {Text} from '#/components/Typography'
+import type * as bsky from '#/types/bsky'
+
+export function GermButton({
+ germ,
+ profile,
+}: {
+ germ: AppBskyActorDefs.ProfileAssociatedGerm
+ profile: bsky.profile.AnyProfileView
+}) {
+ const {_} = useLingui()
+ const t = useTheme()
+ const {currentAccount} = useSession()
+
+ if (germ.showButtonTo === 'none') {
+ return null
+ }
+
+ if (currentAccount?.did === profile.did) {
+ return
+ }
+
+ if (germ.showButtonTo === 'usersIFollow' && !profile.viewer?.followedBy) {
+ return null
+ }
+
+ const url = constructGermUrl(germ, profile, currentAccount?.did)
+
+ if (!url) {
+ return null
+ }
+
+ return (
+
+
+
+ Germ DM
+
+
+
+ )
+}
+
+function GermLogo({size}: {size: 'small' | 'large'}) {
+ return (
+
+ )
+}
+
+function GermSelfButton({did}: {did: string}) {
+ const {_} = useLingui()
+ const selfExplanationDialogControl = Dialog.useDialogControl()
+ const agent = useAgent()
+ const t = useTheme()
+
+ const {mutate, isPending} = useMutation({
+ onMutate: async () => {
+ const res = await agent.com.germnetwork.declaration.get({
+ repo: did,
+ rkey: 'self',
+ })
+
+ if (!res?.value) {
+ throw new Error('Failed to get self declaration')
+ }
+
+ await agent.com.germnetwork.declaration.put(
+ {
+ repo: did,
+ rkey: 'self',
+ },
+ {
+ ...res.value,
+ messageMe: {
+ ...res.value.messageMe,
+ showButtonTo: 'none',
+ },
+ },
+ )
+ },
+ onSuccess: () => {
+ Toast.show(_(msg`Germ DM link removed`))
+ },
+ onError: error => {
+ Toast.show(
+ _(msg`Failed to remove Germ DM link. Error: ${error?.message}`),
+ )
+ if (!isNetworkError(error)) {
+ logger.error('Failed to remove Germ DM link', {safeMessage: error})
+ }
+ },
+ })
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+ Germ DM Link
+
+
+
+
+
+ This button lets others send you a message on the Germ DM app. You
+ can manage its visibility from the Germ DM app.
+
+
+
+
+
+
+
+
+ >
+ )
+}
+
+function constructGermUrl(
+ declaration: AppBskyActorDefs.ProfileAssociatedGerm,
+ profile: bsky.profile.AnyProfileView,
+ viewerDid?: string,
+) {
+ try {
+ const urlp = new URL(declaration.messageMeUrl)
+
+ if (urlp.pathname.endsWith('/')) {
+ urlp.pathname = urlp.pathname.slice(0, -1)
+ }
+
+ urlp.pathname += `/${platform()}`
+
+ if (viewerDid) {
+ urlp.hash = `#${profile.did}+${viewerDid}`
+ } else {
+ urlp.hash = `#${profile.did}`
+ }
+
+ return urlp.toString()
+ } catch {
+ return null
+ }
+}
+
+function platform() {
+ switch (Platform.OS) {
+ case 'ios':
+ return 'iOS'
+ case 'android':
+ return 'android'
+ default:
+ return 'web'
+ }
+}
diff --git a/src/state/session/index.tsx b/src/state/session/index.tsx
index 4a70fc4c09..e63e180a4e 100644
--- a/src/state/session/index.tsx
+++ b/src/state/session/index.tsx
@@ -430,7 +430,7 @@ export function useRequireAuth() {
const {signinDialogControl} = useGlobalDialogsControlContext()
return useCallback(
- (fn: () => void) => {
+ (fn: () => unknown) => {
if (hasSession) {
fn()
} else {