diff --git a/e2e/fixtures/profiles.ts b/e2e/fixtures/profiles.ts index 8377877..e916ba9 100644 --- a/e2e/fixtures/profiles.ts +++ b/e2e/fixtures/profiles.ts @@ -3,11 +3,21 @@ import { test as base, expect } from "@playwright/test"; import { ProfilePage } from "./ProfilePage"; export const test = base.extend<{ - anyProfile: ProfilePage; + v1Profile: ProfilePage; + v1ProfileWithSuffix: ProfilePage; + v2Profile: ProfilePage; }>({ - anyProfile: async ({ page }, use) => { - const publication = new ProfilePage(page, "lensprotocol"); - await use(publication); + v1Profile: async ({ page }, use) => { + const profile = new ProfilePage(page, "lensprotocol"); + await use(profile); + }, + v1ProfileWithSuffix: async ({ page }, use) => { + const profile = new ProfilePage(page, "lensprotocol.lens"); + await use(profile); + }, + v2Profile: async ({ page }, use) => { + const profile = new ProfilePage(page, "lens/lensprotocol"); + await use(profile); }, }); diff --git a/e2e/profiles.spec.ts b/e2e/profiles.spec.ts index 7a309e1..41396d0 100644 --- a/e2e/profiles.spec.ts +++ b/e2e/profiles.spec.ts @@ -6,10 +6,10 @@ test.use(devices["Desktop Chrome"]); test.describe("Given a Profile link", async () => { test.describe("When opening it", async () => { - test("Then it should show relevant app options", async ({ anyProfile }) => { - await anyProfile.open(); + test("Then it should show relevant app options", async ({ v1Profile }) => { + await v1Profile.open(); - await expect(anyProfile.options).toHaveText([ + await expect(v1Profile.options).toHaveText([ "Buttrfly", "Collectz", "Hey", @@ -17,7 +17,7 @@ test.describe("Given a Profile link", async () => { "Lensta", "Riff", "Soclly", - "Tape" + "Tape", ]); }); }); @@ -25,25 +25,25 @@ test.describe("Given a Profile link", async () => { test.describe("Given a Publication link posted on a social media website/app", async () => { test.describe("When checking Open Graph meta tags", async () => { - test("Then it should render the expected base-line meta tags", async ({ anyProfile }) => { - await anyProfile.open(); + test("Then it should render the expected base-line meta tags", async ({ v1Profile }) => { + await v1Profile.open(); - expect(await anyProfile.extractOpenGraphProperties()).toMatchObject({ - "og:title": `@${anyProfile.handle} profile`, + expect(await v1Profile.extractOpenGraphProperties()).toMatchObject({ + "og:title": `@${v1Profile.handle} profile`, "og:description": "The Social Layer for Web3 🌿", - "og:url": expect.stringContaining(`/u/${anyProfile.handle}`), + "og:url": expect.stringContaining(`/u/${v1Profile.handle}`), "og:site_name": "Lens Share", "og:type": "profile", }); }); - test("Then it should include the expected Twitter Card meta tags", async ({ anyProfile }) => { - await anyProfile.open(); + test("Then it should include the expected Twitter Card meta tags", async ({ v1Profile }) => { + await v1Profile.open(); - expect(await anyProfile.extractTwitterMetaTags()).toEqual({ + expect(await v1Profile.extractTwitterMetaTags()).toEqual({ "twitter:card": "summary_large_image", "twitter:site": "LensProtocol", - "twitter:title": `@${anyProfile.handle} profile`, + "twitter:title": `@${v1Profile.handle} profile`, "twitter:description": "The Social Layer for Web3 🌿", "twitter:image": expect.any(String), "twitter:image:type": "image/png", @@ -53,26 +53,26 @@ test.describe("Given a Publication link posted on a social media website/app", a test.describe("When the link includes the `by` attribution param", async () => { test("Then it should mention the originating app in page `title` and Open Graph `site_name` tag", async ({ - anyProfile, + v1Profile, }) => { - await anyProfile.openAsSharedBy("Hey"); + await v1Profile.openAsSharedBy("Hey"); - expect(await anyProfile.getTitle()).toContain("Hey"); - expect(await anyProfile.extractOpenGraphProperties()).toMatchObject({ + expect(await v1Profile.getTitle()).toContain("Hey"); + expect(await v1Profile.extractOpenGraphProperties()).toMatchObject({ "og:site_name": "Hey", }); }); test("Then it should mention the originating app in Twitter Card `site` if a Twitter handle is provided in the app manifest", async ({ - anyProfile, + v1Profile, }) => { - await anyProfile.openAsSharedBy("Hey"); + await v1Profile.openAsSharedBy("Hey"); - expect(await anyProfile.getTitle()).toContain("Hey"); - expect(await anyProfile.extractOpenGraphProperties()).toMatchObject({ + expect(await v1Profile.getTitle()).toContain("Hey"); + expect(await v1Profile.extractOpenGraphProperties()).toMatchObject({ "og:site_name": "Hey", }); - expect(await anyProfile.extractTwitterMetaTags()).toMatchObject({ + expect(await v1Profile.extractTwitterMetaTags()).toMatchObject({ "twitter:site": "heydotxyz", }); }); @@ -81,10 +81,10 @@ test.describe("Given a Publication link posted on a social media website/app", a test.describe("Given a Profile link with `by` attribution param", async () => { test.describe("When opening it", async () => { - test("Then it should show the specified app first", async ({ anyProfile }) => { - await anyProfile.openAsSharedBy("Hey"); + test("Then it should show the specified app first", async ({ v1Profile }) => { + await v1Profile.openAsSharedBy("Hey"); - await expect(anyProfile.options).toHaveText([ + await expect(v1Profile.options).toHaveText([ "Hey", "Buttrfly", "Collectz", @@ -99,33 +99,33 @@ test.describe("Given a Profile link with `by` attribution param", async () => { test.describe("When opening it on a platform not supported by the specified app", async () => { test("Then it should show a message an attribution message before offering other options", async ({ - anyProfile, + v1Profile, }) => { - await anyProfile.openAsSharedBy("orb"); + await v1Profile.openAsSharedBy("orb"); - await expect(anyProfile.context).toHaveText("Shared via Orb mobile app."); + await expect(v1Profile.context).toHaveText("Shared via Orb mobile app."); }); }); }); test.describe("Given an opened Profile link", async () => { test.describe("When submitting an app choice", async () => { - test("Then it should open the publication with the selected app", async ({ anyProfile }) => { - await anyProfile.open(); - const url = await anyProfile.justOnce("Hey"); + test("Then it should open the publication with the selected app", async ({ v1Profile }) => { + await v1Profile.open(); + const url = await v1Profile.justOnce("Hey"); - await expect(url).toMatch(`https://hey.xyz/u/${anyProfile.handle}`); + await expect(url).toMatch(`https://hey.xyz/u/${v1Profile.handle}`); }); }); test.describe("When submitting an app choice with 'Remember' checkbox selected", async () => { - test("Then it should use the same app for all future publications", async ({ anyProfile }) => { - await anyProfile.open(); - await anyProfile.remember("Hey"); + test("Then it should use the same app for all future publications", async ({ v1Profile }) => { + await v1Profile.open(); + await v1Profile.remember("Hey"); - const response = await anyProfile.open(); + const response = await v1Profile.open(); - await expect(response?.url()).toMatch(`https://hey.xyz/u/${anyProfile.handle}`); + await expect(response?.url()).toMatch(`https://hey.xyz/u/${v1Profile.handle}`); }); }); }); diff --git a/src/app/u/[handle]/opengraph-image.tsx b/src/app/u/[handle]/opengraph-image.tsx index 0905e7d..2347c35 100644 --- a/src/app/u/[handle]/opengraph-image.tsx +++ b/src/app/u/[handle]/opengraph-image.tsx @@ -13,6 +13,7 @@ import { ImageResponse } from "next/server"; import { client } from "@/app/client"; import { gintoNordMediumData } from "@/app/fonts"; import { formatProfileHandle } from "@/formatters"; +import { mapV1HandleToV2 } from "@/utils/handle"; import { resolveMediaUrl } from "@/utils/media"; export type ImageProps = { @@ -56,7 +57,7 @@ function resolveProfileImage(profile: ProfileFragment) { } export default async function Image({ params }: ImageProps) { - const profile = await client.profile.fetch({ forHandle: params.handle }); + const profile = await client.profile.fetch({ forHandle: mapV1HandleToV2(params.handle) }); if (!profile) notFound(); diff --git a/src/app/u/[handle]/page.tsx b/src/app/u/[handle]/page.tsx index d45fe72..740dac3 100644 --- a/src/app/u/[handle]/page.tsx +++ b/src/app/u/[handle]/page.tsx @@ -11,6 +11,7 @@ import { twitterHandle } from "@/config"; import { AppManifest, findApp, findFavoriteApp, findProfileApps } from "@/data"; import { formatProfileHandle } from "@/formatters"; import { resolvePlatformType } from "@/utils/device"; +import { mapV1HandleToV2 } from "@/utils/handle"; import { resolveAttribution } from "@/utils/request"; import { openWith } from "./actions"; @@ -25,12 +26,13 @@ export type ProfilePageProps = { export default async function ProfilePage({ params, searchParams }: ProfilePageProps) { const platform = resolvePlatformType(); - const profile = await client.profile.fetch({ forHandle: params.handle }); + const handle = mapV1HandleToV2(params.handle); + const profile = await client.profile.fetch({ forHandle: handle }); const favoriteApp = await findFavoriteApp({ platform }); if (favoriteApp) { - redirectTo(favoriteApp, params.handle); + redirectTo(favoriteApp, handle); } if (!profile) notFound(); @@ -72,7 +74,7 @@ export async function generateMetadata( { params, searchParams }: ProfilePageProps, parent: ResolvingMetadata ) { - const profile = await client.profile.fetch({ forHandle: params.handle }); + const profile = await client.profile.fetch({ forHandle: mapV1HandleToV2(params.handle) }); if (!profile) notFound(); diff --git a/src/utils/handle.ts b/src/utils/handle.ts new file mode 100644 index 0000000..a479953 --- /dev/null +++ b/src/utils/handle.ts @@ -0,0 +1,19 @@ +const V1_SUFFIX = ".lens"; +const V2_NAMESPACE = "lens"; + +function isV1Handle(handle: string): boolean { + return handle.endsWith(".lens"); +} + +/** + * v1 handle format: `handle.lens` + * v2 handle format: `namespace/handle` + */ +export function mapV1HandleToV2(handle: string): string { + if (isV1Handle(handle)) { + const localName = handle.slice(0, -V1_SUFFIX.length); + return `${V2_NAMESPACE}/${localName}`; + } + + return handle; +}