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

Customers architecture #4590

Merged
merged 47 commits into from
Dec 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
b870493
server: create Customer model and start to link logic to it
frankie567 Dec 3, 2024
f104f68
server/customer: implement CRUD API
frankie567 Dec 3, 2024
6af56cd
server: fix order and subscription API to work with customers
frankie567 Dec 3, 2024
9937a92
server/checkout: fix logic and tests related to customers changes
frankie567 Dec 3, 2024
3fb5129
server/order: fix logic and tests related to customers changes
frankie567 Dec 3, 2024
1ec5051
server/subscription: fix logic and tests related to customers changes
frankie567 Dec 3, 2024
e79a022
server/benefit: fix logic and tests related to customers changes
frankie567 Dec 3, 2024
be36c0d
server/checkout: allow to preset Customer when creating session
frankie567 Dec 4, 2024
0bb5667
server: make customer unique by email and organization
frankie567 Dec 5, 2024
5c36457
server/benefit: add oauth acconts to Customer and revamp Discord/GitH…
frankie567 Dec 5, 2024
2084ffe
server: revamp users/ endpoints into /customer-portal endpoints
frankie567 Dec 5, 2024
cdbf51a
server: reintroduce Transaction.payment_user for backward compatibili…
frankie567 Dec 5, 2024
71e8bed
server: add UserCustomer association table and use it in customer por…
frankie567 Dec 5, 2024
89e7108
server/customer: implement oauth account connection
frankie567 Dec 6, 2024
1330499
server/customer_session: implement customer authentication mechanism
frankie567 Dec 6, 2024
d2322ac
server/checkout: generate a customer session after confirmed checkotu
frankie567 Dec 6, 2024
8a92227
server/auth: dynamically add the customer session dependency so it's …
frankie567 Dec 9, 2024
56cf8f6
server/customer_portal: add API to update grants
frankie567 Dec 9, 2024
208d0f4
server: remove user ads endpoints
frankie567 Dec 9, 2024
f84e3f5
server: store the legacy user id on customer and use it in schemas
frankie567 Dec 10, 2024
e4f2996
server/benefit: handle discord refresh token error
frankie567 Dec 10, 2024
34da9b5
server: implement customers migration script
frankie567 Dec 10, 2024
8d7f375
server: wire customer router
frankie567 Dec 10, 2024
714a25e
server/customer_portal: add checkout_id filter to benefit grant list …
frankie567 Dec 11, 2024
4174b0c
server/customer_portal: fix scope for CustomerPortalRead authenticator
frankie567 Dec 11, 2024
3639cc6
server/customer_portal: tweak benefit grant schema
frankie567 Dec 11, 2024
11e5841
server/benefit: remove legacy configuration for GitHub repository
frankie567 Dec 11, 2024
3ae7e54
server/customer_portal: don't require auth when downloading file
frankie567 Dec 11, 2024
f63db51
server/customer_portal: add benefit_id filter on grants list
frankie567 Dec 11, 2024
def8b25
server/customer_portal: filter out revoked benefit grants
frankie567 Dec 11, 2024
1616efc
server/storefront: tweak customer output
frankie567 Dec 11, 2024
fb4d094
server/customer_portal: tweak OAuth accounts endpoints so it works be…
frankie567 Dec 12, 2024
f99d382
server/order: fix error when handling one time purchases invoice
frankie567 Dec 12, 2024
0b8359e
server/checkout: handle authenticated user properly during checkout
frankie567 Dec 12, 2024
f3702b1
server/customer_portal: add a SSE endpoint for customer
frankie567 Dec 12, 2024
527e042
server/customer_portal: add an OTP code mechanism to generate custome…
frankie567 Dec 13, 2024
fcfca9c
server/customer: fix schema names
frankie567 Dec 13, 2024
cf39443
server: generate a customer session when sending order/subscription c…
frankie567 Dec 13, 2024
feb333d
server: make Customer.stripe_customer_id non unique
frankie567 Dec 16, 2024
8930d71
clients/sdk: update OpenAPI client
frankie567 Dec 11, 2024
c444559
clients/web: move things for Customers API
frankie567 Dec 11, 2024
0f726bd
clients/web: implement GitHub/Discord benefit grant handling
frankie567 Dec 12, 2024
096eadb
clients/web: implement more complete customer portal
frankie567 Dec 12, 2024
0301683
clients/web: use dedicated library to connect to SSE
frankie567 Dec 12, 2024
b40c891
clients/web: listen to stream to reload benefits as they are granted …
frankie567 Dec 12, 2024
0d2785d
clients/web: revamp customer portal in its own layout and authenticat…
frankie567 Dec 13, 2024
98b2e29
clients/web: implement customers dashboard
frankie567 Dec 13, 2024
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 clients/apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
"d3": "^7.9.0",
"d3-scale-chromatic": "^3.1.0",
"date-fns": "^3.6.0",
"event-source-plus": "^0.1.8",
"eventemitter3": "^5.0.1",
"framer-motion": "^10.18.0",
"geist": "^1.3.1",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
'use client'

import { DownloadableItem as InnerDownloadableItem } from '@/components/Benefit/Downloadables/SubscriberWidget'
import { DownloadableItem as InnerDownloadableItem } from '@/components/Benefit/Downloadables/DownloadablesBenefitGrant'
import Pagination from '@/components/Pagination/Pagination'
import { PurchasesQueryParametersContext } from '@/components/Purchases/PurchasesQueryParametersContext'
import PurchaseSidebar from '@/components/Purchases/PurchasesSidebar'
import { useUserBenefit, useUserDownloadables } from '@/hooks/queries'
import {
useCustomerBenefitGrants,
useCustomerDownloadables,
} from '@/hooks/queries'
import { api } from '@/utils/api'
import { FileDownloadOutlined } from '@mui/icons-material'
import { DownloadableRead } from '@polar-sh/sdk'
import Link from 'next/link'
Expand All @@ -30,7 +34,7 @@ export default function ClientPage() {
[setPurchaseParameters],
)

const { data: downloadables } = useUserDownloadables({
const { data: downloadables } = useCustomerDownloadables(api, {
limit: purchaseParameters.limit,
page: purchaseParameters.page,
})
Expand Down Expand Up @@ -85,7 +89,12 @@ interface DownloadableItemProps {
}

const DownloadableItem = ({ downloadable }: DownloadableItemProps) => {
const { data: benefit } = useUserBenefit(downloadable.benefit_id)
const { data: benefitGrants } = useCustomerBenefitGrants(api, {
limit: 1,
benefitId: downloadable.benefit_id,
})
const benefitGrant = benefitGrants?.items[0]
const benefit = benefitGrant?.benefit

const organizationLink = useMemo(() => {
if (benefit?.organization.profile_settings?.enabled) {
Expand All @@ -94,7 +103,7 @@ const DownloadableItem = ({ downloadable }: DownloadableItemProps) => {
className="dark:text-polar-500 dark:hover:text-polar-200 text-sm text-gray-500 hover:text-gray-700"
href={`/${benefit.organization.slug}`}
>
{benefit?.organization.name}
{benefit.organization.name}
</Link>
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,13 @@ import { LicenseKeyDetails } from '@/components/Benefit/LicenseKeys/LicenseKeyDe
import Pagination from '@/components/Pagination/Pagination'
import { PurchasesQueryParametersContext } from '@/components/Purchases/PurchasesQueryParametersContext'
import PurchaseSidebar from '@/components/Purchases/PurchasesSidebar'
import { useUserBenefit, useUserLicenseKeys } from '@/hooks/queries'
import {
useCustomerBenefitGrants,
useCustomerLicenseKey,
} from '@/hooks/queries'
import { api } from '@/utils/api'
import { Key } from '@mui/icons-material'
import { LicenseKeyRead } from '@polar-sh/sdk'
import { BenefitType, CustomerBenefitGrantLicenseKeys } from '@polar-sh/sdk'
import Link from 'next/link'
import { useSearchParams } from 'next/navigation'
import Avatar from 'polarkit/components/ui/atoms/avatar'
Expand All @@ -31,9 +35,10 @@ export default function ClientPage() {
[setPurchaseParameters],
)

const { data: licenseKeys } = useUserLicenseKeys({
const { data: benefitGrants } = useCustomerBenefitGrants(api, {
limit: purchaseParameters.limit,
page: purchaseParameters.page,
type: BenefitType.LICENSE_KEYS,
})

return (
Expand All @@ -47,7 +52,7 @@ export default function ClientPage() {
<h3 className="text-2xl">License Keys</h3>
</div>

{licenseKeys?.pagination.total_count === 0 ? (
{benefitGrants?.pagination.total_count === 0 ? (
<div className="flex h-full w-full flex-col items-center gap-y-4 py-32 text-6xl">
<Key
className="dark:text-polar-600 text-gray-400"
Expand All @@ -59,12 +64,15 @@ export default function ClientPage() {
</div>
) : (
<div className="flex w-full flex-col gap-y-6">
{licenseKeys?.items.map((licenseKey) => (
<LicenseKeyItem key={licenseKey.id} licenseKey={licenseKey} />
{benefitGrants?.items.map((benefitGrant) => (
<LicenseKeyItem
key={benefitGrant.id}
benefitGrant={benefitGrant as CustomerBenefitGrantLicenseKeys}
/>
))}
<Pagination
currentPage={purchaseParameters.page}
totalCount={licenseKeys?.pagination.total_count || 0}
totalCount={benefitGrants?.pagination.total_count || 0}
pageSize={purchaseParameters.limit}
onPageChange={onPageChange}
currentURL={searchParams}
Expand All @@ -77,27 +85,31 @@ export default function ClientPage() {
}

interface LicenseKeyItemProps {
licenseKey: LicenseKeyRead
benefitGrant: CustomerBenefitGrantLicenseKeys
}

const LicenseKeyItem = ({ licenseKey }: LicenseKeyItemProps) => {
const { data: benefit } = useUserBenefit(licenseKey.benefit_id)
const LicenseKeyItem = ({ benefitGrant }: LicenseKeyItemProps) => {
const { benefit } = benefitGrant
const { data: licenseKey } = useCustomerLicenseKey(
api,
benefitGrant.properties.license_key_id as string,
)

const organizationLink = useMemo(() => {
if (benefit?.organization.profile_settings?.enabled) {
if (benefit.organization.profile_settings?.enabled) {
return (
<Link
className="dark:text-polar-500 dark:hover:text-polar-200 text-sm text-gray-500 hover:text-gray-700"
href={`/${benefit.organization.slug}`}
>
{benefit?.organization.name}
{benefit.organization.name}
</Link>
)
}

return (
<span className="dark:text-polar-500 text-sm text-gray-500">
{benefit?.organization.name}
{benefit.organization.name}
</span>
)
}, [benefit])
Expand All @@ -117,11 +129,15 @@ const LicenseKeyItem = ({ licenseKey }: LicenseKeyItemProps) => {
</div>
<span className="text-xl">{benefit?.description}</span>
</div>
<div className="flex flex-col gap-y-6">
<CopyToClipboardInput value={licenseKey.key} />
<LicenseKeyDetails className="bg-white" licenseKey={licenseKey} />
</div>
<LicenseKeyActivations licenseKeyId={licenseKey.id} />
{licenseKey && (
<>
<div className="flex flex-col gap-y-6">
<CopyToClipboardInput value={licenseKey.key} />
<LicenseKeyDetails licenseKey={licenseKey} />
</div>
<LicenseKeyActivations api={api} licenseKey={licenseKey} />
</>
)}
</ShadowBox>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ import Pagination from '@/components/Pagination/Pagination'
import { PurchasesQueryParametersContext } from '@/components/Purchases/PurchasesQueryParametersContext'
import PurchaseSidebar from '@/components/Purchases/PurchasesSidebar'
import AmountLabel from '@/components/Shared/AmountLabel'
import { useUserOrders } from '@/hooks/queries'
import { useCustomerOrders } from '@/hooks/queries'
import { api } from '@/utils/api'
import { Search, ShoppingBagOutlined } from '@mui/icons-material'
import { ProductPriceType, UserOrder } from '@polar-sh/sdk'
import { CustomerOrder, ProductPriceType } from '@polar-sh/sdk'
import Link from 'next/link'
import { useSearchParams } from 'next/navigation'
import Button from 'polarkit/components/ui/atoms/button'
Expand All @@ -31,7 +32,7 @@ export default function ClientPage() {
[setPurchaseParameters],
)

const { data: orders } = useUserOrders({
const { data: orders } = useCustomerOrders(api, {
productPriceType: ProductPriceType.ONE_TIME,
query: purchaseParameters.query,
limit: purchaseParameters.limit,
Expand Down Expand Up @@ -102,7 +103,7 @@ export default function ClientPage() {
)
}

const OrderItem = ({ order }: { order: UserOrder }) => {
const OrderItem = ({ order }: { order: CustomerOrder }) => {
const organization = order.product.organization

return (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,40 +1,12 @@
'use client'

import BenefitDetails from '@/components/Benefit/BenefitDetails'
import { BenefitRow } from '@/components/Benefit/BenefitRow'
import { InlineModal } from '@/components/Modal/InlineModal'
import { useUserBenefits, useUserOrderInvoice } from '@/hooks/queries'
import { markdownOptions } from '@/utils/markdown'
import { organizationPageLink } from '@/utils/nav'
import CustomerPortalOrder from '@/components/CustomerPortal/CustomerPortalOrder'
import { api } from '@/utils/api'
import { ArrowBackOutlined } from '@mui/icons-material'
import { UserBenefit, UserOrder } from '@polar-sh/sdk'
import Markdown from 'markdown-to-jsx'
import { CustomerOrder } from '@polar-sh/sdk'
import Link from 'next/link'
import Avatar from 'polarkit/components/ui/atoms/avatar'
import Button from 'polarkit/components/ui/atoms/button'
import { List, ListItem } from 'polarkit/components/ui/atoms/list'
import ShadowBox from 'polarkit/components/ui/atoms/shadowbox'
import { formatCurrencyAndAmount } from 'polarkit/lib/money'
import { useCallback, useState } from 'react'

const ClientPage = ({ order }: { order: UserOrder }) => {
const organization = order.product.organization
const { data: benefits } = useUserBenefits({
orderId: order.id,
limit: 100,
sorting: ['type'],
})

const [selectedBenefit, setSelectedBenefit] = useState<UserBenefit | null>(
null,
)

const orderInvoiceMutation = useUserOrderInvoice()
const openInvoice = useCallback(async () => {
const { url } = await orderInvoiceMutation.mutateAsync(order.id)
window.open(url, '_blank')
}, [orderInvoiceMutation, order])

const ClientPage = ({ order }: { order: CustomerOrder }) => {
return (
<div className="flex flex-col gap-y-8">
<Link
Expand All @@ -44,106 +16,7 @@ const ClientPage = ({ order }: { order: UserOrder }) => {
<ArrowBackOutlined fontSize="inherit" />
<span>Back to Purchases</span>
</Link>
<div className="flex h-full flex-grow flex-col-reverse gap-12 md:flex-row md:items-start">
<div className="flex w-full flex-col gap-8 md:w-2/3">
<ShadowBox className="flex flex-col gap-6">
{organization && (
<Link
className="flex flex-row items-center gap-x-4"
href={`/${organization.slug}`}
>
<Avatar
className="h-12 w-12"
avatar_url={organization.avatar_url}
name={organization.name}
/>
<h3 className="text-lg">{organization.name}</h3>
</Link>
)}
<h1 className="text-3xl font-medium">{order.product.name}</h1>
{order.product.description ? (
<div className="prose dark:prose-invert prose-headings:mt-8 prose-headings:font-semibold prose-headings:text-black prose-h1:text-3xl prose-h2:text-2xl prose-h3:text-xl prose-h4:text-lg prose-h5:text-md prose-h6:text-sm dark:prose-headings:text-polar-50 dark:text-polar-300 max-w-4xl text-gray-800">
<Markdown options={markdownOptions}>
{order.product.description}
</Markdown>
</div>
) : (
<></>
)}
</ShadowBox>
{(benefits?.items.length ?? 0) > 0 && (
<div className="flex flex-col gap-4">
<h3 className="text-lg font-medium">Benefits</h3>
<List>
{benefits?.items.map((benefit) => (
<ListItem
key={benefit.id}
selected={benefit.id === selectedBenefit?.id}
onSelect={() => setSelectedBenefit(benefit)}
>
<BenefitRow benefit={benefit} order={order} />
</ListItem>
))}
</List>
</div>
)}
</div>

<div className="flex w-full flex-col gap-8 md:max-w-[340px]">
<ShadowBox className="flex flex-col gap-8">
<h3 className="text-lg font-medium">{order.product.name}</h3>
<div className="flex flex-col gap-4">
<h1 className="text-4xl font-light">
{formatCurrencyAndAmount(order.amount, order.currency, 0)}
</h1>
<p className="dark:text-polar-500 text-sm text-gray-400">
Purchased on{' '}
{new Date(order.created_at).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
})}
</p>
</div>
<div className="flex flex-col gap-2">
<Button
size="lg"
fullWidth
onClick={openInvoice}
loading={orderInvoiceMutation.isPending}
disabled={orderInvoiceMutation.isPending}
>
Download Invoice
</Button>
{organization &&
organization.profile_settings?.enabled &&
!order.product.is_archived && (
<Link
href={organizationPageLink(
organization,
`products/${order.product.id}`,
)}
>
<Button size="lg" variant="ghost" fullWidth>
Go to Product
</Button>
</Link>
)}
</div>
</ShadowBox>
</div>
</div>
<InlineModal
isShown={selectedBenefit !== null}
hide={() => setSelectedBenefit(null)}
modalContent={
<div className="px-8 py-10">
{selectedBenefit && (
<BenefitDetails benefit={selectedBenefit} order={order} />
)}
</div>
}
/>
<CustomerPortalOrder api={api} order={order} />
</div>
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import { getServerSideAPI } from '@/utils/api/serverside'
import { ResponseError, UserOrder } from '@polar-sh/sdk'
import { CustomerOrder, ResponseError } from '@polar-sh/sdk'
import { notFound } from 'next/navigation'
import ClientPage from './ClientPage'

export default async function Page({ params }: { params: { id: string } }) {
const api = getServerSideAPI()

let order: UserOrder
let order: CustomerOrder

try {
order = await api.usersOrders.get({ id: params.id })
order = await api.customerPortalOrders.get({ id: params.id })
} catch (e) {
if (e instanceof ResponseError && e.response.status === 404) {
notFound()
Expand Down
Loading
Loading