Skip to content

Commit 55b04b0

Browse files
Add logo.dev for business emails (#8292)
* Add `logo.dev` for business emails * Set explicit size parameter * Fall back to logo.dev when website is set on organization * Immediately revalidate SWR cache after organization changes * Remove `key` on `avatar_url` mapping * Fix code review * Add env var to opt in to Logo.dev behavior
1 parent a5db8b0 commit 55b04b0

File tree

22 files changed

+150
-33
lines changed

22 files changed

+150
-33
lines changed

clients/apps/web/next.config.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ const baseCSP = `
2424
frame-src 'self' https://*.js.stripe.com https://js.stripe.com https://hooks.stripe.com https://customer-wl21dabnj6qtvcai.cloudflarestream.com videodelivery.net *.cloudflarestream.com;
2525
script-src 'self' 'unsafe-eval' 'unsafe-inline' https://*.js.stripe.com https://js.stripe.com https://maps.googleapis.com https://www.googletagmanager.com https://chat.cdn-plain.com https://embed.cloudflarestream.com;
2626
style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
27-
img-src 'self' blob: data: https://www.gravatar.com https://lh3.googleusercontent.com https://avatars.githubusercontent.com ${S3_PUBLIC_IMAGES_BUCKET_ORIGIN} https://prod-uk-services-workspac-workspacefilespublicbuck-vs4gjqpqjkh6.s3.amazonaws.com https://prod-uk-services-attachm-attachmentsbucket28b3ccf-uwfssb4vt2us.s3.eu-west-2.amazonaws.com https://i0.wp.com;
27+
img-src 'self' blob: data: https://www.gravatar.com https://img.logo.dev https://lh3.googleusercontent.com https://avatars.githubusercontent.com ${S3_PUBLIC_IMAGES_BUCKET_ORIGIN} https://prod-uk-services-workspac-workspacefilespublicbuck-vs4gjqpqjkh6.s3.amazonaws.com https://prod-uk-services-attachm-attachmentsbucket28b3ccf-uwfssb4vt2us.s3.eu-west-2.amazonaws.com https://i0.wp.com;
2828
font-src 'self';
2929
object-src 'none';
3030
base-uri 'self';

clients/apps/web/src/app/(main)/dashboard/(create)/create/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ export default async function Page(props: {
4343
await revalidate(`organizations:${organization.slug}`)
4444
await revalidate(`storefront:${organization.slug}`)
4545
const currentUser = await getAuthenticatedUser()
46-
await revalidate(`users:${currentUser?.id}:organizations`)
46+
await revalidate(`users:${currentUser?.id}:organizations`, { expire: 0 })
4747
return redirect(`/dashboard/${organization.slug}/onboarding/product`)
4848
}
4949
}

clients/apps/web/src/app/actions.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
import { revalidateTag } from 'next/cache'
44

55
export default async function revalidate(
6-
tag: string,
7-
cacheProfile: string = 'default',
6+
tag: Parameters<typeof revalidateTag>[0],
7+
cacheProfile: Parameters<typeof revalidateTag>[1] = 'default',
88
) {
99
revalidateTag(tag, cacheProfile)
1010
}

clients/apps/web/src/components/Onboarding/OrganizationStep.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,9 @@ export const OrganizationStep = ({
121121
return
122122
}
123123

124-
await revalidate(`users:${currentUser?.id}:organizations`)
124+
await revalidate(`users:${currentUser?.id}:organizations`, {
125+
expire: 0,
126+
})
125127
setUserOrganizations((orgs) => [...orgs, organization])
126128

127129
let queryParams = ''

clients/apps/web/src/components/Settings/OrganizationProfileSettings.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { useAuth } from '@/hooks'
12
import { useUpdateOrganization } from '@/hooks/queries'
23
import { useAutoSave } from '@/hooks/useAutoSave'
34
import { setValidationErrors } from '@/utils/api/errors'
@@ -608,6 +609,8 @@ const OrganizationProfileSettings: React.FC<
608609
const { handleSubmit, setError, formState, reset } = form
609610
const inKYCMode = kyc === true
610611

612+
const { currentUser } = useAuth()
613+
611614
const updateOrganization = useUpdateOrganization()
612615

613616
const onSave = async (body: schemas['OrganizationUpdate']) => {
@@ -625,6 +628,7 @@ const OrganizationProfileSettings: React.FC<
625628
const { data, error } = await updateOrganization.mutateAsync({
626629
id: organization.id,
627630
body: cleanedBody,
631+
userId: currentUser?.id,
628632
})
629633

630634
if (error) {

clients/apps/web/src/hooks/queries/org.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,13 +66,14 @@ export const useUpdateOrganization = () =>
6666
mutationFn: (variables: {
6767
id: string
6868
body: schemas['OrganizationUpdate']
69+
userId?: string
6970
}) => {
7071
return api.PATCH('/v1/organizations/{id}', {
7172
params: { path: { id: variables.id } },
7273
body: variables.body,
7374
})
7475
},
75-
onSuccess: async (result, _variables, _ctx) => {
76+
onSuccess: async (result, variables) => {
7677
const { data, error } = result
7778
if (error) {
7879
return
@@ -82,6 +83,12 @@ export const useUpdateOrganization = () =>
8283
})
8384
await revalidate(`organizations:${data.id}`)
8485
await revalidate(`organizations:${data.slug}`)
86+
87+
if (variables.userId) {
88+
await revalidate(`users:${variables.userId}:organizations`, {
89+
expire: 0,
90+
})
91+
}
8592
},
8693
})
8794

server/polar/config.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,25 @@ class Settings(BaseSettings):
236236
# Loops
237237
LOOPS_API_KEY: str | None = None
238238

239+
# Logo.dev (for company logo avatars)
240+
LOGO_DEV_PUBLISHABLE_KEY: str | None = None
241+
PERSONAL_EMAIL_DOMAINS: set[str] = {
242+
"gmail.com",
243+
"yahoo.com",
244+
"hotmail.com",
245+
"outlook.com",
246+
"aol.com",
247+
"icloud.com",
248+
"mail.com",
249+
"protonmail.com",
250+
"zoho.com",
251+
"gmx.com",
252+
"yandex.com",
253+
"msn.com",
254+
"live.com",
255+
"qq.com",
256+
}
257+
239258
# Logfire
240259
LOGFIRE_TOKEN: str | None = None
241260
LOGFIRE_IGNORED_ACTORS: set[str] = {

server/polar/customer/schemas/customer.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from fastapi import Path
77
from pydantic import UUID4, Field, computed_field
88

9+
from polar.config import settings
910
from polar.kit.address import Address, AddressInput
1011
from polar.kit.email import EmailStrDNS
1112
from polar.kit.metadata import (
@@ -127,8 +128,16 @@ class CustomerBase(MetadataOutputMixin, TimestampedSchema, IDSchema):
127128

128129
@computed_field(examples=["https://www.gravatar.com/avatar/xxx?d=404"])
129130
def avatar_url(self) -> str:
130-
email_hash = hashlib.sha256(self.email.lower().encode()).hexdigest()
131-
return f"https://www.gravatar.com/avatar/{email_hash}?d=404"
131+
domain = self.email.split("@")[-1].lower()
132+
133+
if (
134+
not settings.LOGO_DEV_PUBLISHABLE_KEY
135+
or domain in settings.PERSONAL_EMAIL_DOMAINS
136+
):
137+
email_hash = hashlib.sha256(self.email.lower().encode()).hexdigest()
138+
return f"https://www.gravatar.com/avatar/{email_hash}?d=404"
139+
140+
return f"https://img.logo.dev/{domain}?size=64&retina=true&token={settings.LOGO_DEV_PUBLISHABLE_KEY}&fallback=404"
132141

133142

134143
class Customer(CustomerBase):

server/polar/models/organization.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from datetime import datetime
22
from enum import StrEnum
33
from typing import TYPE_CHECKING, Any, Self, TypedDict
4+
from urllib.parse import urlparse
45
from uuid import UUID
56

67
from sqlalchemy import (
@@ -147,10 +148,31 @@ class Organization(RateLimitGroupMixin, RecordModel):
147148

148149
name: Mapped[str] = mapped_column(String, nullable=False, index=True)
149150
slug: Mapped[str] = mapped_column(CITEXT, nullable=False, unique=True)
150-
avatar_url: Mapped[str | None] = mapped_column(String, nullable=True)
151+
_avatar_url: Mapped[str | None] = mapped_column(
152+
String, name="avatar_url", nullable=True
153+
)
151154

152155
email: Mapped[str | None] = mapped_column(String, nullable=True, default=None)
153156
website: Mapped[str | None] = mapped_column(String, nullable=True, default=None)
157+
158+
@property
159+
def avatar_url(self) -> str | None:
160+
if self._avatar_url:
161+
return self._avatar_url
162+
163+
if not self.website or not settings.LOGO_DEV_PUBLISHABLE_KEY:
164+
return None
165+
166+
parsed = urlparse(self.website)
167+
domain = parsed.netloc or parsed.path
168+
domain = domain.lower().removeprefix("www.")
169+
170+
return f"https://img.logo.dev/{domain}?size=64&retina=true&token={settings.LOGO_DEV_PUBLISHABLE_KEY}&fallback=404"
171+
172+
@avatar_url.setter
173+
def avatar_url(self, value: str | None) -> None:
174+
self._avatar_url = value
175+
154176
socials: Mapped[list[OrganizationSocials]] = mapped_column(
155177
JSONB, nullable=False, default=list
156178
)

server/polar/organization/schemas.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,15 @@ def validate_reserved_keywords(value: str) -> str:
5959
]
6060

6161

62+
def _discard_logo_dev_url(url: HttpUrl) -> HttpUrl | None:
63+
if url.host and url.host.endswith("logo.dev"):
64+
return None
65+
return url
66+
67+
68+
AvatarUrl = Annotated[HttpUrlToStr, AfterValidator(_discard_logo_dev_url)]
69+
70+
6271
class OrganizationFeatureSettings(Schema):
6372
issue_funding_enabled: bool = Field(
6473
False, description="If this organization has issue funding enabled"
@@ -314,7 +323,7 @@ class Organization(OrganizationBase):
314323
class OrganizationCreate(Schema):
315324
name: NameInput
316325
slug: SlugInput
317-
avatar_url: HttpUrlToStr | None = None
326+
avatar_url: AvatarUrl | None = None
318327
email: EmailStrDNS | None = Field(None, description="Public support email.")
319328
website: HttpUrlToStr | None = Field(
320329
None, description="Official website of the organization."
@@ -335,7 +344,7 @@ class OrganizationCreate(Schema):
335344

336345
class OrganizationUpdate(Schema):
337346
name: NameInput | None = None
338-
avatar_url: HttpUrlToStr | None = None
347+
avatar_url: AvatarUrl | None = None
339348

340349
email: EmailStrDNS | None = Field(None, description="Public support email.")
341350
website: HttpUrlToStr | None = Field(

0 commit comments

Comments
 (0)