Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 3 additions & 9 deletions packages/shared/src/features/interests/hooks/useShareAgent.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,11 @@
import { useState } from 'react';
import { useShareOrCopyLink } from '../../../hooks/useShareOrCopyLink';
import { webappUrl } from '../../../lib/constants';
import { getAbsoluteWebappUrl } from '../../../lib/links';
import { ReferralCampaignKey } from '../../../lib/referral';
import type { UserInterest } from '../../../graphql/interests';

export const agentShareLink = (query: string): string => {
const path = `${webappUrl}agent?q=${encodeURIComponent(query)}`;
// Must be absolute: the share pipeline runs `new URL(link)`, and `webappUrl`
// is a bare path in development, which threw rather than degrading.
const origin = globalThis?.location?.origin;

return origin ? new URL(path, origin).toString() : path;
};
export const agentShareLink = (query: string): string =>
getAbsoluteWebappUrl(`agent?q=${encodeURIComponent(query)}`);

export const useShareAgent = (
interest?: Pick<UserInterest, 'query'>,
Expand Down
3 changes: 2 additions & 1 deletion packages/shared/src/hooks/useSourceMenuProps.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useRouter } from 'next/router';
import { useContentPreference } from './contentPreference/useContentPreference';
import { webappUrl } from '../lib/constants';
import { getAbsoluteWebappUrl } from '../lib/links';
import type { SourceTooltip } from '../graphql/sources';
import { ContentPreferenceType } from '../graphql/contentPreference';
import { LogEvent } from '../lib/log';
Expand Down Expand Up @@ -58,7 +59,7 @@ const useSourceMenuProps = ({
text: source?.handle
? `Check out ${source.handle} on daily.dev`
: 'Check out this source on daily.dev',
link: source?.permalink || webappUrl,
link: source?.permalink || getAbsoluteWebappUrl(),
cid: ReferralCampaignKey.ShareSource,
logObject: () => ({
event_name: LogEvent.ShareSource,
Expand Down
33 changes: 32 additions & 1 deletion packages/shared/src/lib/links.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { getPathnameWithQuery, withHttps } from './links';
import { getAbsoluteWebappUrl, getPathnameWithQuery, withHttps } from './links';

describe('lib/links tests', () => {
it('should return links as https links', () => {
Expand Down Expand Up @@ -51,3 +51,34 @@ describe('getPathnameWithQuery', () => {
expect(getPathnameWithQuery('/foo? ', 'a=1')).toBe('/foo?a=1');
});
});

describe('getAbsoluteWebappUrl', () => {
// The test setup mirrors the webapp, where `webappUrl` is a bare `/`.
it('resolves the path against the page origin', () => {
expect(getAbsoluteWebappUrl('tools/docker')).toBe(
`${globalThis.location.origin}/tools/docker`,
);
});

it('points at the home page without a path', () => {
expect(getAbsoluteWebappUrl()).toBe(`${globalThis.location.origin}/`);
});

it('leaves an absolute webappUrl, as on the extension, alone', () => {
const previous = process.env.NEXT_PUBLIC_WEBAPP_URL;
process.env.NEXT_PUBLIC_WEBAPP_URL = 'https://app.daily.dev/';

try {
jest.isolateModules(() => {
// eslint-disable-next-line global-require, @typescript-eslint/no-var-requires
const links = require('./links');

expect(links.getAbsoluteWebappUrl('world/ido')).toBe(
'https://app.daily.dev/world/ido',
);
});
} finally {
process.env.NEXT_PUBLIC_WEBAPP_URL = previous;
}
});
});
10 changes: 10 additions & 0 deletions packages/shared/src/lib/links.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,16 @@ export const getPathnameWithQuery = (
export const toWebappHref = (path: string): string =>
path.startsWith('/') ? `${webappUrl}${path.slice(1)}` : path;

// For links that leave the tab (share, copy): `webappUrl` is a bare `/` on the
// webapp, and the share pipeline runs `new URL(link)` on whatever it is handed.
// An absolute `webappUrl` (the extension) passes through unchanged.
export const getAbsoluteWebappUrl = (path = ''): string => {
const url = `${webappUrl}${path}`;
const origin = globalThis?.location?.origin;

return origin ? new URL(url, origin).toString() : url;
};

export const agentsHighlightsPath = '/highlights/vibes';

export const agentsHighlightsUrl = `${webappUrl}${agentsHighlightsPath.slice(
Expand Down
2 changes: 2 additions & 0 deletions packages/shared/src/lib/referral.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,6 @@ export enum ReferralCampaignKey {
ShareTag = 'share_tag',
ShareAgent = 'share_agent',
ShareSlack = 'share_slack',
ShareWorld = 'share_world',
ShareTool = 'share_tool',
}
18 changes: 17 additions & 1 deletion packages/webapp/__tests__/ToolPage.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React from 'react';
import nock from 'nock';
import type { NextRouter } from 'next/router';
import type { RenderResult } from '@testing-library/react';
import { render, screen } from '@testing-library/react';
import { act, fireEvent, render, screen } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import AuthContext from '@dailydotdev/shared/src/contexts/AuthContext';
import type { LoggedUser } from '@dailydotdev/shared/src/lib/user';
Expand Down Expand Up @@ -269,3 +269,19 @@ it('should render squads with the directory card details', async () => {
expect(screen.getByText(/@platform/)).toBeInTheDocument();
expect(screen.getByText('42 members')).toBeInTheDocument();
});

it('should copy an absolute, tracked link to the tool', async () => {
const writeText = jest.fn().mockResolvedValue(undefined);
Object.assign(navigator, { clipboard: { writeText } });
renderComponent(defaultProps, loggedUser);

const share = await screen.findByRole('button', { name: 'Share' });
await act(async () => {
fireEvent.click(share);
});

// `webappUrl` is a bare `/` on the webapp, which pastes as a dead link.
expect(writeText).toHaveBeenCalledWith(
`${globalThis.location.origin}/tools/docker?userid=${loggedUser.id}&cid=share_tool`,
);
});
49 changes: 43 additions & 6 deletions packages/webapp/__tests__/WorldShare.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
import type { ReactElement } from 'react';
import React from 'react';
import { fireEvent, render as rtlRender, screen } from '@testing-library/react';
import {
act,
fireEvent,
render as rtlRender,
screen,
} from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import type { PublicProfile } from '@dailydotdev/shared/src/lib/user';
import { LogEvent } from '@dailydotdev/shared/src/lib/log';
import { ReferralCampaignKey } from '@dailydotdev/shared/src/lib/referral';
import { ShareProvider } from '@dailydotdev/shared/src/lib/share';
import { TestBootProvider } from '@dailydotdev/shared/__tests__/helpers/boot';
import loggedUser from '@dailydotdev/shared/__tests__/fixture/loggedUser';
import { WorldShare } from '../components/world/WorldShare';

const mockShareOrCopy = jest.fn();
Expand All @@ -14,6 +22,10 @@ jest.mock('@dailydotdev/shared/src/hooks/useShareOrCopyLink', () => ({
useShareOrCopyLink: (props: unknown) => mockUseShareOrCopyLink(props),
}));

const { useShareOrCopyLink: actualUseShareOrCopyLink } = jest.requireActual(
'@dailydotdev/shared/src/hooks/useShareOrCopyLink',
);

const user = {
id: 'u1',
username: 'ido',
Expand All @@ -23,6 +35,7 @@ const user = {
interface ShareArgs {
link: string;
text: string;
cid?: ReferralCampaignKey;
logObject: (provider: ShareProvider) => Record<string, unknown>;
}

Expand All @@ -45,18 +58,42 @@ describe('WorldShare', () => {
it('hands out an absolute link to the world', () => {
render(<WorldShare user={user} isOwn={false} />);

// Absolute on purpose: the whole point is a link that survives the tab.
expect(argsOf().link).toBe(
`${process.env.NEXT_PUBLIC_WEBAPP_URL}world/ido`,
);
// Absolute on purpose: the whole point is a link that survives the tab,
// and `webappUrl` is a bare `/` on the webapp.
expect(argsOf().link).toBe(`${globalThis.location.origin}/world/ido`);
});

it('falls back to the id for a reader with no username', () => {
render(
<WorldShare user={{ ...user, username: undefined }} isOwn={false} />,
);

expect(argsOf().link).toBe(`${process.env.NEXT_PUBLIC_WEBAPP_URL}world/u1`);
expect(argsOf().link).toBe(`${globalThis.location.origin}/world/u1`);
});

it('tags the link with the world share campaign', () => {
render(<WorldShare user={user} isOwn={false} />);

expect(argsOf().cid).toBe(ReferralCampaignKey.ShareWorld);
});

it('copies a link that still works once pasted', async () => {
const writeText = jest.fn().mockResolvedValue(undefined);
Object.assign(navigator, { clipboard: { writeText } });
mockUseShareOrCopyLink.mockImplementation(actualUseShareOrCopyLink);
rtlRender(
<TestBootProvider client={new QueryClient()} auth={{ user: loggedUser }}>
<WorldShare user={user} isOwn={false} />
</TestBootProvider>,
);

await act(async () => {
fireEvent.click(screen.getByRole('button', { name: 'Share this world' }));
});

expect(writeText).toHaveBeenCalledWith(
`${globalThis.location.origin}/world/ido?userid=${loggedUser.id}&cid=share_world`,
);
});

it('names the owner when the world belongs to somebody else', () => {
Expand Down
6 changes: 4 additions & 2 deletions packages/webapp/components/world/WorldShare.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ import {
} from '@dailydotdev/shared/src/components/buttons/Button';
import { Tooltip } from '@dailydotdev/shared/src/components/tooltip/Tooltip';
import { useShareOrCopyLink } from '@dailydotdev/shared/src/hooks/useShareOrCopyLink';
import { webappUrl } from '@dailydotdev/shared/src/lib/constants';
import { getAbsoluteWebappUrl } from '@dailydotdev/shared/src/lib/links';
import { LogEvent } from '@dailydotdev/shared/src/lib/log';
import { ReferralCampaignKey } from '@dailydotdev/shared/src/lib/referral';
import type { ShareProvider } from '@dailydotdev/shared/src/lib/share';

interface WorldShareProps {
Expand Down Expand Up @@ -43,7 +44,7 @@ export function WorldShare({
}: WorldShareProps): ReactElement {
const whose = isOwn ? 'my' : `${user.name}'s`;
const [copying, onShareOrCopy] = useShareOrCopyLink({
link: `${webappUrl}world/${user.username || user.id}`,
link: getAbsoluteWebappUrl(`world/${user.username || user.id}`),
text: worldName
? `Check out ${worldName}, ${whose} world on daily.dev`
: `Check out ${whose} world on daily.dev`,
Expand All @@ -52,6 +53,7 @@ export function WorldShare({
target_id: user.id,
extra: JSON.stringify({ provider }),
}),
cid: ReferralCampaignKey.ShareWorld,
});

return (
Expand Down
2 changes: 2 additions & 0 deletions packages/webapp/pages/join/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ const componentsMap: ReferralRecord<FunctionComponent<JoinPageProps>> = {
[ReferralCampaignKey.ShareTag]: Referral,
[ReferralCampaignKey.ShareAgent]: Referral,
[ReferralCampaignKey.ShareSlack]: Referral,
[ReferralCampaignKey.ShareWorld]: Referral,
[ReferralCampaignKey.ShareTool]: Referral,
};

const referralCampaignValues = new Set<string>(
Expand Down
10 changes: 7 additions & 3 deletions packages/webapp/pages/tools/[slug].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,10 @@ import { useShareOrCopyLink } from '@dailydotdev/shared/src/hooks/useShareOrCopy
import { anchorDefaultRel } from '@dailydotdev/shared/src/lib/strings';
import { largeNumberFormat } from '@dailydotdev/shared/src/lib/numberFormat';
import { publishTimeRelativeShort } from '@dailydotdev/shared/src/lib/dateFormat';
import { webappUrl } from '@dailydotdev/shared/src/lib/constants';
import { getDomainFromUrl } from '@dailydotdev/shared/src/lib/links';
import {
getAbsoluteWebappUrl,
getDomainFromUrl,
} from '@dailydotdev/shared/src/lib/links';
import {
ProfileImageSize,
ProfilePicture,
Expand All @@ -101,6 +103,7 @@ import { ProfilePictureGroup } from '@dailydotdev/shared/src/components/ProfileP
import { ToolLogo } from '@dailydotdev/shared/src/components/tools/ToolLogo';
import { useLogContext } from '@dailydotdev/shared/src/contexts/LogContext';
import { LogEvent, Origin, TargetType } from '@dailydotdev/shared/src/lib/log';
import { ReferralCampaignKey } from '@dailydotdev/shared/src/lib/referral';
import { ActiveFeedNameContext } from '@dailydotdev/shared/src/contexts';
import { FeedLayoutProvider } from '@dailydotdev/shared/src/contexts/FeedContext';
import { TAG_FEED_QUERY } from '@dailydotdev/shared/src/graphql/feed';
Expand Down Expand Up @@ -356,13 +359,14 @@ const ToolPage = ({
);

const [copying, onShareOrCopy] = useShareOrCopyLink({
link: `${webappUrl}tools/${tool.slug}`,
link: getAbsoluteWebappUrl(`tools/${tool.slug}`),
text: `Check out ${tool.title} on daily.dev`,
logObject: (provider) => ({
event_name: LogEvent.ShareTool,
target_id: tool.slug,
extra: JSON.stringify({ provider, origin: Origin.ToolPage }),
}),
cid: ReferralCampaignKey.ShareTool,
});

const { data: followedStackers } = useQuery({
Expand Down
Loading