Skip to content

Commit

Permalink
Merge pull request #4380 from tloncorp/lb/invite-fixes-130
Browse files Browse the repository at this point in the history
Invite Fixes
  • Loading branch information
latter-bolden authored Jan 30, 2025
2 parents 8a2cfb6 + aea27c7 commit bacb7b3
Show file tree
Hide file tree
Showing 8 changed files with 143 additions and 36 deletions.
30 changes: 20 additions & 10 deletions apps/tlon-mobile/src/hooks/useDeepLinkListener.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import { useBranch, useSignupParams } from '@tloncorp/app/contexts/branch';
import { useShip } from '@tloncorp/app/contexts/ship';
import { RootStackParamList } from '@tloncorp/app/navigation/types';
import { useTypedReset } from '@tloncorp/app/navigation/utils';
import { createDevLogger } from '@tloncorp/shared';
import { AnalyticsEvent, createDevLogger } from '@tloncorp/shared';
import * as store from '@tloncorp/shared/store';
import { useEffect, useRef } from 'react';

const logger = createDevLogger('deeplinkHandler', true);
Expand All @@ -21,10 +22,14 @@ export const useDeepLinkListener = () => {
(async () => {
isHandlingLinkRef.current = true;
logger.log(`handling deep link`, lure, signupParams);
logger.trackEvent(AnalyticsEvent.InviteDebug, {
context: 'Handling deeplink click',
lure: lure.id,
});
try {
// if the lure was clicked prior to authenticating, trigger the automatic join & DM
if (lure.shouldAutoJoin) {
// no-op for now, hosting will handle
if (lure.shouldAutoJoin || !ship) {
// if the lure was clicked prior to authenticating, no-op for now.
// Hosting will handle once the user signs up.
} else {
// otherwise, treat it as a deeplink and navigate
if (lure.inviteType === 'user') {
Expand All @@ -49,12 +54,17 @@ export const useDeepLinkListener = () => {
`handling deep link to invited group`,
lure.invitedGroupId
);
reset([
{
name: 'ChatList',
params: { previewGroupId: lure.invitedGroupId },
},
]);

store.redeemInviteIfNeeded(lure);
const previewGroupId = lure.invitedGroupId || lure.group;
if (previewGroupId) {
reset([
{
name: 'ChatList',
params: { previewGroupId },
},
]);
}
}
}
} catch (e) {
Expand Down
6 changes: 6 additions & 0 deletions apps/tlon-mobile/src/hooks/useOnboardingHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,12 @@ export function useOnboardingHelpers() {
// clear native managed cookie since we set manually
await clearHostingNativeCookie();

logger.trackEvent(AnalyticsEvent.UserLoggedIn, {
email: params.email,
phoneNumber: params.phoneNumber,
client: 'tlon-mobile',
});

if (maybeAccountIssue) {
switch (maybeAccountIssue) {
// If the account has no assigned ship, treat it as a signup
Expand Down
46 changes: 28 additions & 18 deletions packages/app/contexts/branch.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { createDevLogger } from '@tloncorp/shared';
import { AnalyticsEvent, createDevLogger } from '@tloncorp/shared';
import { storage } from '@tloncorp/shared/db';
import { AppInvite, Lure, extractLureMetadata } from '@tloncorp/shared/logic';
import {
Expand Down Expand Up @@ -111,26 +111,36 @@ export const BranchProvider = ({ children }: { children: ReactNode }) => {

// Handle Branch link click
if (params?.['+clicked_branch_link']) {
logger.log('detected Branch link click');
logger.trackEvent('Detected Branch Link Click', {
inviteId: params.lure,
});

if (params.lure) {
// Link had a lure field embedded
logger.log('detected lure link:', params.lure);
const nextLure: Lure = {
lure: {
...extractLureMetadata(params),
id: params.lure as string,
// if not already authenticated, we should run Lure's invite auto-join capability after signing in
shouldAutoJoin: !isAuthenticated,
},
priorityToken: params.token as string | undefined,
};
console.log(`setting deeplink lure`, nextLure);
setState({
...nextLure,
deepLinkPath: undefined,
});
storage.invitation.setValue(nextLure);
try {
const nextLure: Lure = {
lure: {
...extractLureMetadata(params),
id: params.lure as string,
// if not already authenticated, we should run Lure's invite auto-join capability after signing in
shouldAutoJoin: !isAuthenticated,
},
priorityToken: params.token as string | undefined,
};
console.log(`setting deeplink lure`, nextLure);
setState({
...nextLure,
deepLinkPath: undefined,
});
storage.invitation.setValue(nextLure);
} catch (e) {
logger.trackError(AnalyticsEvent.InviteError, {
context: 'Failed to extract lure metadata',
inviteId: params.lure,
errorMessage: e?.message,
});
}
} else if (params.wer) {
// Link had a wer (deep link) field embedded
const deepLinkPath = getPathFromWer(params.wer as string);
Expand Down Expand Up @@ -161,7 +171,7 @@ export const BranchProvider = ({ children }: { children: ReactNode }) => {
console.debug('[branch] Unsubscribing from Branch listener');
unsubscribe();
};
}, [isAuthenticated]);
}, [goToChannel, isAuthenticated]);

const setLure = useCallback(
(invite: AppInvite) => {
Expand Down
4 changes: 4 additions & 0 deletions packages/app/contexts/ship.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import crashlytics from '@react-native-firebase/crashlytics';
import { AnalyticsEvent, createDevLogger } from '@tloncorp/shared';
import { ShipInfo, storage } from '@tloncorp/shared/db';
import { preSig } from '@urbit/aura';
import type { ReactNode } from 'react';
Expand All @@ -13,6 +14,8 @@ import { NativeModules } from 'react-native';

import { transformShipURL } from '../utils/string';

const logger = createDevLogger('useShip', false);

const { UrbitModule } = NativeModules;

type State = ShipInfo & {
Expand Down Expand Up @@ -111,6 +114,7 @@ export const ShipProvider = ({ children }: { children: ReactNode }) => {
})();
}

logger.trackEvent(AnalyticsEvent.NodeAuthSaved);
setIsLoading(false);
},
[]
Expand Down
2 changes: 2 additions & 0 deletions packages/shared/src/domain/analytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,6 @@ export enum AnalyticsEvent {
NodeConnectionDebug = 'Node Connection Debug',
NodeConnectionError = 'Node Connection Error',
SyncDiscontinuity = 'Sync Discontinuity',
UserLoggedIn = 'User Logged In',
NodeAuthSaved = 'Node Auth Saved',
}
34 changes: 29 additions & 5 deletions packages/shared/src/logic/branch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,9 @@ export interface DeepLinkMetadata {
invitedGroupIconImageUrl?: string;
invitedGroupiconImageColor?: string;
inviteType?: 'user' | 'group';
group?: string; // legacy identifier for invitedGroupId
inviter?: string; // legacy identifier for inviterUserId
image?: string; // legacy identifier for invitedGroupIconImageUrl
}

export interface AppInvite extends DeepLinkMetadata {
Expand All @@ -103,18 +106,39 @@ export function extractLureMetadata(branchParams: any) {
return {};
}

return {
inviterUserId: branchParams.inviterUserId,
const extracted = {
inviterUserId: branchParams.inviterUserId || branchParams.inviter,
inviterNickname: branchParams.inviterNickname,
inviterAvatarImage: branchParams.inviterAvatarImage,
inviterColor: branchParams.inviterColor,
invitedGroupId: branchParams.invitedGroupId,
invitedGroupTitle: branchParams.invitedGroupTitle,
invitedGroupId: branchParams.invitedGroupId ?? branchParams.group, // only fallback to key if invitedGroupId missing, not empty
invitedGroupTitle: branchParams.invitedGroupTitle || branchParams.title,
invitedGroupDescription: branchParams.invitedGroupDescription,
invitedGroupIconImageUrl: branchParams.invitedGroupIconImageUrl,
invitedGroupIconImageUrl:
branchParams.invitedGroupIconImageUrl || branchParams.image,
invitedGroupiconImageColor: branchParams.invitedGroupiconImageColor,
inviteType: branchParams.inviteType,
};

if (
!extracted.inviterUserId &&
!extracted.invitedGroupId &&
branchParams.lure &&
branchParams.lure.includes('/')
) {
// fall back to v1 style lures where the id is a flag
const [ship, _] = branchParams.lure.split('/');
if (isValidPatp(ship)) {
extracted.inviterUserId = ship;
extracted.invitedGroupId = branchParams.lure;
}
}

if (!extracted.inviterUserId && !extracted.invitedGroupId) {
throw new Error('Failed to extract valid lure metadata');
}

return extracted;
}

export function isLureMeta(input: unknown): input is DeepLinkMetadata {
Expand Down
51 changes: 51 additions & 0 deletions packages/shared/src/store/hostingActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import * as domain from '../domain';
import { AnalyticsEvent } from '../domain';
import * as logic from '../logic';
import { withRetry } from '../logic';
import { syncGroupPreviews } from './sync';

const logger = createDevLogger('hostingActions', true);

Expand Down Expand Up @@ -270,3 +271,53 @@ export async function authenticateWithReadyNode(): Promise<db.ShipInfo | null> {
authType: 'hosted',
};
}

export async function redeemInviteIfNeeded(invite: logic.AppInvite) {
const currentUserId = api.getCurrentUserId();
if (invite.inviteType && invite.inviteType === 'user') {
return;
}

const groupId = invite.invitedGroupId || invite.group;
if (!groupId) {
logger.trackEvent(AnalyticsEvent.InviteError, {
context: 'Invite missing group identifier',
inviteId: invite.id,
});
return;
}

const group = await db.getGroup({ id: groupId });

if (!group) {
syncGroupPreviews([groupId]);
}

const isJoined = group && group.currentUserIsMember;
const haveInvite = group && group.haveInvite;
const shouldRedeem = !isJoined && !haveInvite;

if (shouldRedeem) {
try {
await api.inviteShipWithLure({ ship: currentUserId, lure: invite.id });
logger.trackEvent(AnalyticsEvent.InviteDebug, {
context: 'Success, bit invite deeplink lure while logged in',
lure: invite.id,
});
} catch (err) {
logger.trackEvent(AnalyticsEvent.InviteError, {
context: 'Failed to bite lure on invite deeplink while logged in',
lure: invite.id,
errorMessage: err.message,
});
}
} else {
logger.trackEvent(AnalyticsEvent.InviteDebug, {
context: 'Invite redemption not needed, skipping',
inviteId: invite.id,
isJoined,
haveInvite,
shouldRedeem,
});
}
}
6 changes: 3 additions & 3 deletions packages/ui/src/components/Onboarding/OnboardingInvite.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export const OnboardingInviteBlock = React.memo(function OnboardingInviteBlock({
}

const inviter = {
id: inviterUserId!,
id: inviterUserId,
nickname: inviterNickname,
avatarImage: inviterAvatarImage,
color: inviterColor || undefined,
Expand Down Expand Up @@ -107,10 +107,10 @@ function GroupInvite({
/>
<ListItem.MainContent>
<ListItem.Title>
Join {groupShim.title ?? groupShim.id}
{groupShim.title ? `Join ${groupShim.title}` : `Join a Groupchat`}
</ListItem.Title>
<ListItem.Subtitle>
Invited by {inviter.nickname ?? inviter.id}
Invited by {getDisplayName(inviter)}
</ListItem.Subtitle>
</ListItem.MainContent>
</ListItem>
Expand Down

0 comments on commit bacb7b3

Please sign in to comment.