diff --git a/connector/src/platforms/linkedin.js b/connector/src/platforms/linkedin.js index 20c039e..6caa1dc 100644 --- a/connector/src/platforms/linkedin.js +++ b/connector/src/platforms/linkedin.js @@ -54,24 +54,28 @@ export default { console: "https://www.linkedin.com/developers/apps", steps: [ "Create an app at linkedin.com/developers/apps and link it to your company's LinkedIn Page (the Page admin must verify it).", - "Products tab: add “Sign In with LinkedIn using OpenID Connect” and “Share on LinkedIn” (instant).", - "For posting to clients' company Pages, also request “Community Management API” (LinkedIn reviews it; your company Page must be verified).", + "Products tab: request “Community Management API” for posting to clients' company Pages (LinkedIn reviews it; your company Page must be verified).", "Auth tab: add the Redirect URL shown below, then copy the Client ID and Client Secret here.", ], - note: "Until Community Management API is approved, set Scopes to “openid profile w_member_social” so you can post to personal profiles only.", + note: "Without Community Management API (personal profiles only): add “Sign In with LinkedIn using OpenID Connect” and “Share on LinkedIn”, and set Scopes to “openid profile w_member_social”.", }, auth: { authorizeUrl: "https://www.linkedin.com/oauth/v2/authorization", tokenUrl: "https://www.linkedin.com/oauth/v2/accessToken", - scopes: ["openid", "profile", "w_member_social", "r_organization_admin", "w_organization_social", "r_organization_social"], + // The scopes Community Management API grants (it can't be combined with the OpenID Connect product). + scopes: ["r_basicprofile", "w_member_social", "rw_organization_admin", "w_organization_social", "r_organization_social"], }, formats: { TEXT: true, IMAGE: true, CAROUSEL: true, REELS: true }, limits: { caption: 3000, images: 20 }, targetLabel: "Post as", async identify(tokens) { - const { data } = await request("LinkedIn", `${API}/v2/userinfo`, { token: tokens.accessToken }); - return { id: data.sub, name: data.name, username: null, avatar: data.picture || null, url: null }; + if (/\bopenid\b/.test(tokens.scope || "")) { + const { data } = await request("LinkedIn", `${API}/v2/userinfo`, { token: tokens.accessToken }); + return { id: data.sub, name: data.name, username: null, avatar: data.picture || null, url: null }; + } + const { data } = await request("LinkedIn", `${API}/v2/me`, { token: tokens.accessToken }); + return { id: data.id, name: [data.localizedFirstName, data.localizedLastName].filter(Boolean).join(" ") || "LinkedIn member", username: null, avatar: null, url: null }; }, /** The person, plus every company Page they administer (needs Community Management API). */ diff --git a/connector/test/platforms.test.mjs b/connector/test/platforms.test.mjs index 9e18882..33b7ad7 100644 --- a/connector/test/platforms.test.mjs +++ b/connector/test/platforms.test.mjs @@ -139,7 +139,7 @@ test("sign-in URLs carry the right client id, scopes and PKCE per platform", () test("LinkedIn: connect, list company Pages, post image to a company Page", async () => { reset(); const { conn } = await connect("linkedin", "acme", { identify: [ - ["GET", "https://api.linkedin.com/v2/userinfo", json({ sub: "abc", name: "Pat Lee", picture: "https://p" })], + ["GET", "https://api.linkedin.com/v2/me", json({ id: "abc", localizedFirstName: "Pat", localizedLastName: "Lee" })], ["GET", "https://api.linkedin.com/rest/organizationAcls", json({ elements: [{ organization: "urn:li:organization:42" }] })], ["GET", "https://api.linkedin.com/rest/organizations/42", json({ localizedName: "Acme Co" })], ] });