Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: upgrade to support lens v2 #33

Merged
merged 1 commit into from
Nov 10, 2023
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
1 change: 1 addition & 0 deletions .npmrc
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ engine-strict=true
auto-install-peers=false
strict-peer-dependencies=false
link-workspace-packages=false
node-linker=hoisted # required to fix zod version incompatibility
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,18 +51,18 @@ Some examples:
The Lens Share Link format is:

```
https://share.lens.xyz/u/<handle>[?by=<appId>]
https://share.lens.xyz/u/<fullHandle>[?by=<appId>]
```

Where:

- `<handle>` is the profile handle inclusive of the `.lens` suffix but without the `@` prefix (e.g. `alice.lens`, `bob.lens`, etc.).
- `<fullHandle>` is the profile v2 full handle including the namespace (e.g. `lens/alice`, `lens/bob`, etc.).
- `<appId>` is an optional parameter that reflect the Lens App ID of the app used to generate the Lens Share Link. This is used to give priority to the app that generated the Lens Share Link when the user opens the Lens Share Link.

Some examples:

- `https://share.lens.xyz/u/alice.lens`
- `https://share.lens.xyz/u/alice.lens?by=Hey`
- `https://share.lens.xyz/u/lens/alice`
- `https://share.lens.xyz/u/lens/alice?by=Hey`

## Lens Share UI

Expand Down Expand Up @@ -99,7 +99,7 @@ After cloning the repo, run `pnpm install` to fetch its dependencies. Then you c
- Fork this repository
- Install the dependencies with `pnpm install`
- Add your app manifest to the `manifests` folder
- Run the app locally with `pnpm dev`. The app is available at `http://localhost:3000/u/<your-handle>.lens`, or `http://localhost:3000/p/<your-publication-id>`
- Run the app locally with `pnpm dev`. The app is available at `http://localhost:3000/u/<your-full-handle>`, or `http://localhost:3000/p/<your-publication-id>`
- Test thoroughly your configuration by:
- opening Lens Share links with web and mobile browser and verify your app shows in the list as expected
- opening Lens Share links with `?by=<your-app-id>` parameter to verify your app is prioritized as expected
Expand Down
18 changes: 14 additions & 4 deletions e2e/fixtures/profiles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
},
});

Expand Down
164 changes: 125 additions & 39 deletions e2e/profiles.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,73 +6,137 @@ 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",
"LensFrens",
"Lensta",
"Riff",
"Soclly",
"Tape"
"Tape",
]);
});
});
});

test.describe("Given a Publication link posted on a social media website/app", async () => {
test.describe("Given a v1 Profile 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 ({ v1Profile }) => {
await v1Profile.open();

expect(await v1Profile.extractOpenGraphProperties()).toMatchObject({
"og:title": `lens/${v1Profile.handle} profile`,
"og:description": "The Social Layer for Web3 🌿",
"og:url": expect.stringContaining(`/u/lens/${v1Profile.handle}`),
"og:site_name": "Lens Share",
"og:type": "profile",
});
});

test("Then it should include the expected Twitter Card meta tags", async ({ v1Profile }) => {
await v1Profile.open();

expect(await v1Profile.extractTwitterMetaTags()).toEqual({
"twitter:card": "summary_large_image",
"twitter:site": "LensProtocol",
"twitter:title": `lens/${v1Profile.handle} profile`,
"twitter:description": "The Social Layer for Web3 🌿",
"twitter:image": expect.any(String),
"twitter:image:type": "image/png",
});
});
});
});

test.describe("Given a v1 Profile link with suffix 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 ({
v1ProfileWithSuffix,
}) => {
await v1ProfileWithSuffix.open();

expect(await v1ProfileWithSuffix.extractOpenGraphProperties()).toMatchObject({
"og:title": `lens/lensprotocol profile`,
"og:description": "The Social Layer for Web3 🌿",
"og:url": expect.stringContaining(`/u/lens/lensprotocol`),
"og:site_name": "Lens Share",
"og:type": "profile",
});
});

test("Then it should include the expected Twitter Card meta tags", async ({
v1ProfileWithSuffix,
}) => {
await v1ProfileWithSuffix.open();

expect(await v1ProfileWithSuffix.extractTwitterMetaTags()).toEqual({
"twitter:card": "summary_large_image",
"twitter:site": "LensProtocol",
"twitter:title": `lens/lensprotocol profile`,
"twitter:description": "The Social Layer for Web3 🌿",
"twitter:image": expect.any(String),
"twitter:image:type": "image/png",
});
});
});
});

test.describe("Given a v2 Profile 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 ({ v2Profile }) => {
await v2Profile.open();

expect(await anyProfile.extractOpenGraphProperties()).toMatchObject({
"og:title": `@${anyProfile.handle} profile`,
expect(await v2Profile.extractOpenGraphProperties()).toMatchObject({
"og:title": `${v2Profile.handle} profile`,
"og:description": "The Social Layer for Web3 🌿",
"og:url": expect.stringContaining(`/u/${anyProfile.handle}`),
"og:url": expect.stringContaining(`/u/${v2Profile.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 ({ v2Profile }) => {
await v2Profile.open();

expect(await anyProfile.extractTwitterMetaTags()).toEqual({
expect(await v2Profile.extractTwitterMetaTags()).toEqual({
"twitter:card": "summary_large_image",
"twitter:site": "LensProtocol",
"twitter:title": `@${anyProfile.handle} profile`,
"twitter:title": `${v2Profile.handle} profile`,
"twitter:description": "The Social Layer for Web3 🌿",
"twitter:image": expect.any(String),
"twitter:image:type": "image/png",
});
});
});
});

test.describe("Given a Profile link posted on a social media website/app", async () => {
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",
});
});
Expand All @@ -81,10 +145,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",
Expand All @@ -99,33 +163,55 @@ 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(v1Profile.context).toHaveText("Shared via Orb mobile app.");
});
});
});

test.describe("Given an opened v1 Profile link", async () => {
test.describe("When submitting an app choice", async () => {
test("Then it should open the publication with the selected app", async ({ v1Profile }) => {
await v1Profile.open();
const url = await v1Profile.justOnce("Hey");

expect(url).toMatch(`https://hey.xyz/u/lens/${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 ({ v1Profile }) => {
await v1Profile.open();
await v1Profile.remember("Hey");

const response = await v1Profile.open();

await expect(anyProfile.context).toHaveText("Shared via Orb mobile app.");
expect(response?.url()).toMatch(`https://hey.xyz/u/lens/${v1Profile.handle}`);
});
});
});

test.describe("Given an opened Profile link", async () => {
test.describe("Given an opened v2 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 ({ v2Profile }) => {
await v2Profile.open();
const url = await v2Profile.justOnce("Hey");

await expect(url).toMatch(`https://hey.xyz/u/${anyProfile.handle}`);
expect(url).toMatch(`https://hey.xyz/u/${v2Profile.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 ({ v2Profile }) => {
await v2Profile.open();
await v2Profile.remember("Hey");

const response = await anyProfile.open();
const response = await v2Profile.open();

await expect(response?.url()).toMatch(`https://hey.xyz/u/${anyProfile.handle}`);
expect(response?.url()).toMatch(`https://hey.xyz/u/${v2Profile.handle}`);
});
});
});
27 changes: 7 additions & 20 deletions e2e/publications.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ test.describe("Given a Publication link posted on a social media website/app", a
await textPost.open();

expect(await textPost.extractOpenGraphProperties()).toEqual({
"og:title": "Post by @stani.lens",
"og:title": "Post by lens/stani",
"og:description": "This post will age well.",
"og:url": expect.stringContaining(`/p/${textPost.publicationId}`),
"og:site_name": "Lens Share",
Expand All @@ -42,7 +42,7 @@ test.describe("Given a Publication link posted on a social media website/app", a
expect(await textPost.extractTwitterMetaTags()).toEqual({
"twitter:card": "summary",
"twitter:site": "LensProtocol",
"twitter:title": "Post by @stani.lens",
"twitter:title": "Post by lens/stani",
"twitter:description": "This post will age well.",
});
});
Expand All @@ -53,8 +53,7 @@ test.describe("Given a Publication link posted on a social media website/app", a
await imagePost.open();

expect(await imagePost.extractOpenGraphProperties()).toMatchObject({
"og:image":
"https://ipfs-2.thirdwebcdn.com/ipfs/QmRxDD6oxyWxtTyJoq52C1nUfuWiA5HiseJwksAXPz24BF",
"og:image": "https://gw.ipfs-lens.dev/ipfs/QmRxDD6oxyWxtTyJoq52C1nUfuWiA5HiseJwksAXPz24BF",
"og:image:type": "image/jpeg",
});
});
Expand All @@ -67,7 +66,7 @@ test.describe("Given a Publication link posted on a social media website/app", a
expect(await imagePost.extractTwitterMetaTags()).toMatchObject({
"twitter:card": "summary_large_image",
"twitter:image":
"https://ipfs-2.thirdwebcdn.com/ipfs/QmRxDD6oxyWxtTyJoq52C1nUfuWiA5HiseJwksAXPz24BF",
"https://gw.ipfs-lens.dev/ipfs/QmRxDD6oxyWxtTyJoq52C1nUfuWiA5HiseJwksAXPz24BF",
});
});
});
Expand All @@ -78,7 +77,7 @@ test.describe("Given a Publication link posted on a social media website/app", a

expect(await videoPost.extractOpenGraphProperties()).toMatchObject({
"og:image":
"https://ipfs-2.thirdwebcdn.com/ipfs/bafybeib7o45x6oesq4ziwdatncakvvwqkp6wvur5b45od4fm6gbcbjmce4",
"https://gw.ipfs-lens.dev/ipfs/bafybeib7o45x6oesq4ziwdatncakvvwqkp6wvur5b45od4fm6gbcbjmce4",
});
});
});
Expand Down Expand Up @@ -118,13 +117,7 @@ test.describe("Given a Video Publication link", async () => {
}) => {
await videoPost.open();

await expect(videoPost.options).toHaveText([
"Buttrfly",
"Collectz",
"Hey",
"Soclly",
"Tape",
]);
await expect(videoPost.options).toHaveText(["Buttrfly", "Collectz", "Hey", "Soclly", "Tape"]);
});
});
});
Expand All @@ -134,13 +127,7 @@ test.describe("Given a Publication link with `by` attribution param", async () =
test("Then it should show the specified app first", async ({ videoPost }) => {
await videoPost.openAsSharedBy("tape");

await expect(videoPost.options).toHaveText([
"Tape",
"Buttrfly",
"Collectz",
"Hey",
"Soclly",
]);
await expect(videoPost.options).toHaveText(["Tape", "Buttrfly", "Collectz", "Hey", "Soclly"]);
});
});

Expand Down
6 changes: 6 additions & 0 deletions next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,12 @@ const nextConfig = {
destination: "/u/:handle?by=tape",
permanent: false,
},
// v1 to v2 handle redirect
{
source: "/u/:handle",
destination: "/u/lens/:handle",
permanent: false,
},
];
},
};
Expand Down
Loading
Loading