From 93d2f3a8dbd78a23dcbe3642897474d0f37168e2 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Wed, 11 Feb 2026 23:51:19 +0000 Subject: [PATCH] Germ DM button (#9848) * germ link (wip) * add todos * just yeet declaration for now * ensure not proxied * allow undo delete * tweak styles * ignore unknown values * tweak styles * fix boolean logic * skip IAB * fix mutationFn * add link warning interstitial * Logging and metrics * Little more error handling * Update copy --------- Co-authored-by: Eric Bailey --- assets/images/germ_logo.webp | Bin 0 -> 7840 bytes src/analytics/metrics/types.ts | 5 + src/components/dialogs/LinkWarning.tsx | 19 +- .../Profile/Header/ProfileHeaderStandard.tsx | 5 + src/screens/Profile/components/GermButton.tsx | 334 ++++++++++++++++++ src/state/session/index.tsx | 2 +- 6 files changed, 362 insertions(+), 3 deletions(-) create mode 100644 assets/images/germ_logo.webp create mode 100644 src/screens/Profile/components/GermButton.tsx diff --git a/assets/images/germ_logo.webp b/assets/images/germ_logo.webp new file mode 100644 index 0000000000000000000000000000000000000000..a85048381ac71eee1131022b862b807875f6ad85 GIT binary patch literal 7840 zcmWIYbaR^_$G{No>J$(bV4=_>$G~u53Bz1QtuKsU80{u{EbvxcvRHBHmW+xGLY%32 z4@(wuXusbXfB4_q+m&54)4%KAv;QE^xGn#k{p9_tihCme)y(t%{r{zX_WYNBo$LDk z_WfY~TmE^&SJ`Lm-|Mg4{ujQ6|I@q$^7nT2?PaZstKa$mXz8|NsB*KYwrG#AQ947aOe%)ZJKK9GEL1yqKvZ!4H>^A)AbJd2`-}+#NBM^waKMx3ZH?@5ML%Aa-q;PmCd1Lf7Z^km)hCxK6u#nI^u3t zih1_7oazmakLl!@y_$ZRwdL)Rj_UNCf$ZlT=1K^!ZCQA5+cK7Ti6^tKDD-}=|LrWH z;3~KEhw*{tzszT9{%k$Js!02ti_eWz^}inLOj~a(5njwxx?zfiX$|+g({Xzv7Z1n17O`gJTOVL6*%!bC2GUpv*6!&4&uaGkMhaqYePVXg#caHR4y z|Et@x{uk?2OfY<1+D?2vEbMNpDROyuU45 z|1(~F-^9$5>n@zzWPd)-RN=~>+RwUy+I?p)YyE%p(?2l!{#n)2&qdA`<}LrnyT?Yg z_guHAzpH9^!vek@(|cm!yPsNiIa^(i-4*sJt?t^!@L#5ZejGWA4&*z|*WqBe$)cnK zim6GQ+JSF5-mRIk{QK%3kzD!pj`n|d^L?E-SMG7q1J89qHS-OZ+(_cFFuU-kG5D^= z!i&)p{&<}{bN9jpd-ore4w<$RFND7aM{|1}xxXMF=0v4{dNikV9?u&u8P$blU1=qk zt|}Yf(UiRLx>6*Bjp4y^|NR`S7jC}$8@=#B(`2W)vvc})bnJR{Tjt^fANPjSJl4H> zTl;Jd%oS=lx?JnD+M0f)dds(K%9cH?IkcZs)$Hq+?V4O(UuDl$Y=0TI$cOny`;(?% z&c4H?Yi294ylCC?=;<@wM^5&e;<;zMJg_%D?3&obs0QKe;N^#0?LJTCc@f_KVXj0o zAMfsM`k%}D(#(bPV#6+`$sZ@$?`~hUjwdS3bW>>N@!gv?TORwlXkm6*59h@W zg9o?w2<|_+f+6)#qL#Itz?O;bFC%up<-2GI}$e$M35H6XWb+;z=7>e#y9dPjddiw>}u0QVBct>K3r^7dIkI<}%H7k6JavuF* zbe{G(|J*_@%K)bpLP5o+OrM;I*{K_4`Dguvh;4U|>}OQXYTxwe%L1#5(UnTIH|IF7 z5^UM@xayemgbs$jrQ*?Pi>#!VSf(B;Pf1yK)c9czf1cJh9q(&jI(nDhOl^uc?o5@k zzI!hC<&$mFm)-MP9{uA<(K=?_Qz&XG9=JoUsZ%S;C1ShvCGPXLjptv#bbe6}Kl5#Q zQ-!F|)5VicUT3(LH=o@}Ctj{%rE|#K8*=s=q?`|ZKgXNJv!iK$-~<`thNU@vlFqA6 zA78AUJ!Jv&YtL)yyX-Ij_un@ur(|-d?YEoN{~WiAMX#M{TYuzktH)ul?Bm4@6W`3! zW02Y0`@%YfdG6kwe9%qd0-vb&+Mi{e~aCcF`@iLfT4(b>|#ieS`Vde~|Lk za}{w&n=h)6HSLeVg=dxeMuIP5Kd(QO%nso8>yO-`@s`X;ic zV!wadx#`7*a#tRh2^}apDV6r(=Cn6EQmwPka<6=zpR=j|M5T7DvS`ygflW`+4`qMt z%RhQpTXXBQ2rG-c_dcbwn@>k?ZcR%~e&Z0$xV4~8;B7sx=(dETmwzm`4eCDnQt+h2 z{9nudKaYxHxNCX4VbZ(FucmHrtNYsa*#AguTg<*=t6bA=XJnMCFDeyrOIx+M|A@s- z(cf9`PuSfIN#YSR7OYDAI_ZVt{V8X@cpVDYJJ7EfcjZ$~kyO5Z%q$Ap?HmPN29&U9R>66lh3d%lx^EGL+#gs>FEl0)`YB@ zm3{1EQz4Ug$nF*1#Oe3k!iH)e&v>vPy0U2W#_?^7#U-kQp+LPD8X(isa=Uy?L+&v?Z*Tip3U^(|l2=Zm^&=3QHq0w3R>oMv!sRl%pI ztrshP&5}u4-pN;^|Kp3{2QS5b`#Jn_k!HCG%~wzVaqbqlFi~W|QoqgVKi{iK{u52` z;ypNjo`mTB2|d5GRb$@&$>d%6(bngu`Ibf3!YtxZ3J9zCnP8S8Z> zWQ)wq%bOfnPG_ecl`oogPif_D50>Y*+J zWcjq=at&irH1D77$Ih_mKZxMi-S~QkCg;k$koNMsbN^0JZv3|Dv6#MB$)+0@-uchh z)m^#iZJWJ?^VJvXdl+A^yXk~PCn?{}&WMVWdv_%DY0E*o7mTJ zde#z=MNW;o4?M2Uchu54r@Fn;>d8C1<4va75qFIyi$pLOh^mY1-q&)gN;f3v?%L9z z^PPgXo<iaM+&9%=J&?~Jo~fhkPeXX+eYpRXQ%TE(s|`IEyb;nmYG-|L%d+g5zC znc=)>%aa%xg^bDH8#o?MeZH@IT4`d@RP%>F<~ghN{TJ1_%cf{9)55VXFuU$-9#K1Wo2_u-uTukO!TZg+IH2hU|WU+>dpE0aVgG0b$aRe84kr>de%;?;%wg93sK zKg3(*oIUcIX-C2OkUi$fb)Wz0rd90gpZr2Y^m6#ixD)X{Ce@7sxgnw4iz zG#~Ah64onblT>uFFU{|ad*LLhdbc?5d!k5Y*QqCc9=CB=#UvC+f*z^ZSx zPuCq^@TvAmv3y!dPfFbJmrKPW7Ef3ionxA3FFX0mTG6Yoy6<+JjW9YI624-RwpZW_ z?u2E+xeI=+>ALY^2DeE*gJ0(#K?Ch`-{e<_27Aq^N>p7XXsj`7)~z6){Ek}n_h)MT z157NIEnUpL`swnTNt&?;e*z_qPiQum>g}k$nG?lYB}MZjN`FyUKxQ0 za`yzC{C|6SmiK1qW9A$77CpVb?{j{97rWS%8J7=CGgPYGaQ||}Zyn*AtIo{+o}4u$ zNhL)4cK&gfKQGI54<}yE3aXgC|JphRX}6U=+x97lYEFGMN&Mkkxr`^h-=_8{Z0l-Y zfB(XoNo^nZZ4~ieRIsGz>A}+$T$exPrsdUq-uUFk{8M(1BTTLcpRRH@4{|-9aM($& z_eP-ezgN8me}6rS-r9W1C)}sCFjIi>DtEeFvNXsu;+Ud&{1L@HTx3NjEiS%Izpr}zOWzKEo>?6 ztF9KpadB!x{s{vMGaJuVD{+a!FLT{(<{#Qz__UoxqvpUnyYRbp_pZLTx;W=E+x|mO zI$igFlt@prT6^);e|ipg%(%jMBO^b4!}elM0x_t&O~f6Ek^dw+VVo|Rf=nOzSmdt9)~W?{%mr%Y|D#`M<5t z-ue6S>aNf`J7@h0naZ6szxS?0tCUEWiTBQrc84y{`*2o`-D=LoOA}@u)XG}UJlk$z za{kTpyife(+3nyRl|HCkOAIK7Pr| z`!97}t78#(#B%t!x9Dcq&#$B&Nv=%Wwd5i5^-RIVI!DT`y}IDhxbdml%(W6JQ~C>k zzV6)PXyDG4RbDlF`k5p}oeufW{1;Yp>4b%Ae6}rGR<70_Sp9fI)0(Th4>@lTJ(KSl zY;=IT?ay?v> z8koQTnR%(_Ym;0_?c06-?!D3I&$l#+{&#;{Kyl1f|5-A*jGXd*2|SZ$&hdQ2p~E8~ zx9L8+(N7B%!|vOyd;A_%9eHcN|9*tFk8xq}f)3t4?mxm-t*o2&{7v$ycE2fWq8BZ_ zw7yn^_ljc90sg~}*UWnL`;={~pOS%_?&jjd0u^ePyh`*g-*DJ&nqIEp)_u>V#mQ*L z4#%VKY?aq_-CpBt9T=ll(BR&o9CY+USZjb+@)xZykG_64t1G_kn0;5^E-Qz*$P=4@y4jj#Ro z=Wpkc_Jkio5jR)O4cMf9Xx&X=%fnpF<+~-glRthv-uCL{8l_cCjPG};wKKKMseWPW z)wfAC`IJc52B%8^~Dt9(6 zIQw~rTrzwBO(>w02=Nfo=6t-706P;9ow?}*dWP8WU=^}lU= zNkyS|Px|aXP1{y5sr7Q|nJabjZobyK^1I6>D+SKwEZT-UnvTbPEL_O-J8`#3XPL>o z=~o!1uYKbl$o}m3)uQ4zi78Uszh27^o${cn*5QOn!^<1@oIgL@+RRdUK5?l=&VTD` z@~Ip=#?OB{%%8s`-n=`a;EB!gve>-Y_rDt5^D+rLVY~Bo5sT}awbFlOKeZj}KJj&? z#F}-F?^XD@FmIl<{q?Sm_E*HSRUS3`yYBsCEBmQe{#7Q6viXu0H26;PO;-GNXHJD9 zo71ah@0^!c>~Ycv@#CD8vBz-B^=Wbu_g-H}Sp0XT)V0Z}+9eYnZZ~*3)#LnGvoq?e zC5qO3dUjFnQQC|*k5xk&OZTfY7_5H4mJ&8W-+BMv>Sd0rZ^v=YU!ftYY~8c($bw$) z8;2L08U3?9cxBS|TbDwnw|-GL)v3BKt%qyTdD-fu%MH;ivmS|@REmE5(rKRJ&CkMX zwDxb_77@>}O{5~|{-V{TLHXwvNW04$u79Gn!=JlSB8h*Kz|_ZW{{PG7X*F!RrT)me zYDR+&C+At|+n!ep${Uug5Ks1NnPBte zuf*-DQ?5F%*#&3HJ#f| zZ#*h~a8=5h-w&rL3cD&i%6unq=~?Kb*()w98g={ZJ$JD^be`~P=bqKMY`6c#i7)!a z_4kU|{HvQ!JlKD|=;T`mmPKldnHFdrv~2tEd%`QBiB>kvA)S4v*I!KO%I=x#6d34G z(A{+NwfGU8A8N9e>gl$d0-YD^TRb7my8sytOwS* zS)MeB+?kUf?BrJSR+O)L@+S8KYYx|Gs!Uy)Uy#1GXxi|YYH({}Ii z4{I~f0({JBS&J_n~yQB&FC zS}5n9Ry1LL;jxLc4VtAMKYZ40x+Ce8l7ZEyHO|@&>R$1Oyssq96PC)Y_}6po&x`jn z7U}*m@^I68&izA;b^U`kT)BLUf0Rc^_U8P&dz@=(g-gsG*3D%sOaC@oIxw&tXuB)u zFQhv)_Gq#1O!iL(mid7fQ=d8+&E)!@`~-8ej=p( zZpxuEHe542hL6R**~XTM^D z%$1w#_e?zBx6!{}=y}$e*ygOMOzo*n*(TW)BC!UK$aZCuE{$22@uHXZ&Eo|`XiKoXJ)vw=G@_4Bz$`1VY!C+8TXe}3S5YJ zmuvF0G)VqqkmasBv$(l$=X^dp@2ZY~*PK6VxjjPGUtOl}%J^1m!(8LOu9<%%m>VXj zzf|xlKmQXllwG5|uHWF+^xHcpeMT^hnC0iaGX1HPBHP7@cyr1QV+K2E!$+i zB-6;=!fkonls8-_mj7knuJeT_v3aWh6}yey`(u+k3x6;=pU+Y_ca?j3ZN)T(uTuP3=E7X&Oi4$B{*^8;wp}# zwR4R}OM~ - + ) } -function InAppBrowserConsentInner({ +export function CustomLinkWarningDialog({ + control, + link, +}: { + control: Dialog.DialogControlProps + link?: {href: string; displayText: string; share?: boolean} +}) { + return ( + + + + + ) +} + +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 6467756d4..c60fcd032 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 000000000..c5af7d536 --- /dev/null +++ b/src/screens/Profile/components/GermButton.tsx @@ -0,0 +1,334 @@ +import {Platform, View} from 'react-native' +import {Image} from 'expo-image' +import { + type AppBskyActorDefs, + type AppBskyActorGetProfile, + type AtpAgent, +} from '@atproto/api' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' +import {useMutation, useQueryClient} from '@tanstack/react-query' + +import {until} from '#/lib/async/until' +import {isNetworkError} from '#/lib/strings/errors' +import {RQKEY} from '#/state/queries/profile' +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 {CustomLinkWarningDialog} from '#/components/dialogs/LinkWarning' +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 {useAnalytics} from '#/analytics' +import type * as bsky from '#/types/bsky' + +export function GermButton({ + germ, + profile, +}: { + germ: AppBskyActorDefs.ProfileAssociatedGerm + profile: bsky.profile.AnyProfileView +}) { + const t = useTheme() + const ax = useAnalytics() + const {_} = useLingui() + const {currentAccount} = useSession() + const linkWarningControl = Dialog.useDialogControl() + + // exclude `none` and all unknown values + if ( + !(germ.showButtonTo === 'everyone' || germ.showButtonTo === 'usersIFollow') + ) { + 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 ( + <> + { + ax.metric('profile:associated:germ:click-to-chat', {}) + if (isCustomGermDomain(url)) { + evt.preventDefault() + linkWarningControl.open() + return false + } + }} + label={_(msg`Open Germ DM`)} + overridePresentation={false} + shouldProxy={false} + style={[ + t.atoms.bg_contrast_50, + a.rounded_full, + a.self_start, + {padding: 6}, + ]}> + + + Germ DM + + + + + + ) +} + +function GermLogo({size}: {size: 'small' | 'large'}) { + return ( + + ) +} + +function GermSelfButton({did}: {did: string}) { + const t = useTheme() + const ax = useAnalytics() + const {_} = useLingui() + const selfExplanationDialogControl = Dialog.useDialogControl() + const agent = useAgent() + const queryClient = useQueryClient() + + const {mutate: deleteDeclaration, isPending} = useMutation({ + mutationFn: async () => { + const previousRecord = await agent.com.germnetwork.declaration + .get({ + repo: did, + rkey: 'self', + }) + .then(res => res.value) + .catch(() => null) + + await agent.com.germnetwork.declaration.delete({ + repo: did, + rkey: 'self', + }) + + await whenAppViewReady(agent, did, res => !res.data.associated?.germ) + + return previousRecord + }, + onSuccess: previousRecord => { + ax.metric('profile:associated:germ:self-disconnect', {}) + + async function undo() { + if (!previousRecord) return + try { + await agent.com.germnetwork.declaration.put( + { + repo: did, + rkey: 'self', + }, + previousRecord, + ) + await whenAppViewReady(agent, did, res => !!res.data.associated?.germ) + await queryClient.refetchQueries({queryKey: RQKEY(did)}) + + Toast.show(_(msg`Germ DM reconnected`)) + ax.metric('profile:associated:germ:self-reconnect', {}) + } catch (e: any) { + Toast.show( + _(msg`Failed to reconnect Germ DM. Error: ${e?.message}`), + { + type: 'error', + }, + ) + if (!isNetworkError(e)) { + ax.logger.error('Failed to reconnect Germ DM link', { + safeMessage: e, + }) + } + } + } + + selfExplanationDialogControl.close(() => { + void queryClient.refetchQueries({queryKey: RQKEY(did)}) + Toast.show( + + + + Germ DM disconnected + + {previousRecord && ( + void undo()}> + Undo + + )} + , + ) + }) + }, + onError: error => { + Toast.show( + _(msg`Failed to disconnect Germ DM. Error: ${error?.message}`), + { + type: 'error', + }, + ) + if (!isNetworkError(error)) { + ax.logger.error('Failed to disconnect Germ DM link', { + safeMessage: error, + }) + } + }, + }) + + return ( + <> + + + + + + + + + Germ DM Link + + + + + + This button lets others open the Germ DM app to send you a + message. You can manage its visibility from the Germ DM app, or + you can disconnect your Bluesky account from Germ DM altogether by + clicking the button below. + + + + + + + + + + ) +} + +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 isCustomGermDomain(url: string) { + try { + const urlp = new URL(url) + return urlp.hostname !== 'landing.ger.mx' + } catch { + return false + } +} + +function platform() { + switch (Platform.OS) { + case 'ios': + return 'iOS' + case 'android': + return 'android' + default: + return 'web' + } +} + +async function whenAppViewReady( + agent: AtpAgent, + actor: string, + fn: (res: AppBskyActorGetProfile.Response) => boolean, +) { + await until( + 5, // 5 tries + 1e3, // 1s delay between tries + fn, + () => agent.app.bsky.actor.getProfile({actor}), + ) +} diff --git a/src/state/session/index.tsx b/src/state/session/index.tsx index 4a70fc4c0..e63e180a4 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 { -- 2.51.2