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

Update polar extension #15974

Merged
merged 5 commits into from
Dec 20, 2024
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
4 changes: 3 additions & 1 deletion extensions/polar/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
# Polar Changelog

## 2024-12-20
## [Update] - 2024-12-20

- Implements a View Customers command
- Properly checks if scopes are sufficient on authorization
- Make sure to open Order on the correct page on https://polar.sh

## [Initial Version] - 2024-12-10
8 changes: 4 additions & 4 deletions extensions/polar/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 9 additions & 1 deletion extensions/polar/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,18 @@
"description": "View your active subscriptions",
"mode": "view",
"icon": "command-icon.png"
},
{
"name": "customers",
"title": "View Customers",
"subtitle": "Polar",
"description": "View your customers",
"mode": "view",
"icon": "command-icon.png"
}
],
"dependencies": {
"@polar-sh/sdk": "^0.18.1",
"@polar-sh/sdk": "^0.19.2",
"@raycast/api": "^1.69.0",
"@raycast/utils": "^1.18.1",
"@tanstack/react-query": "^5.62.3",
Expand Down
145 changes: 145 additions & 0 deletions extensions/polar/src/customers.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import { Action, ActionPanel, Detail, List } from "@raycast/api";
import React, { useCallback, useEffect, useState } from "react";
import { authenticate } from "./oauth";
import { PolarProvider, queryClient } from "./providers";
import { QueryClientProvider } from "@tanstack/react-query";
import { useCustomers } from "./hooks/customers";
import { Customer } from "@polar-sh/sdk/models/components";
import { useOrganization } from "./hooks/organizations";

export default function Command() {
const [accessToken, setAccessToken] = useState<string>();

useEffect(() => {
authenticate().then(setAccessToken);
}, []);

if (!accessToken) {
return <Detail isLoading={true} markdown="Authenticating with Polar..." />;
}

return (
<QueryClientProvider client={queryClient}>
<PolarProvider accessToken={accessToken}>
<CustomersView />
</PolarProvider>
</QueryClientProvider>
);
}

interface CustomerProps {
customer: Customer;
}

const CustomerItem = ({ customer }: CustomerProps) => {
const { data: organization } = useOrganization(customer.organizationId);

return (
<List.Item
key={customer.id}
title={customer.email}
detail={
<List.Item.Detail
metadata={
<List.Item.Detail.Metadata>
<List.Item.Detail.Metadata.Label
title="Email"
text={customer.email}
/>
<List.Item.Detail.Metadata.Label
title="Name"
text={customer.name ?? "—"}
/>
<List.Item.Detail.Metadata.Label
title="First Seen"
text={new Date(customer.createdAt).toLocaleDateString("en-US", {
year: "numeric",
month: "long",
day: "numeric",
hour: "numeric",
minute: "numeric",
})}
/>
<List.Item.Detail.Metadata.Label
title="Organization"
text={organization?.name ?? "—"}
/>
<List.Item.Detail.Metadata.Separator />
{/* Add billing info */}
<List.Item.Detail.Metadata.Label
title="Address 1"
text={customer.billingAddress?.line1 ?? "—"}
/>
<List.Item.Detail.Metadata.Label
title="Address 2"
text={customer.billingAddress?.line2 ?? "—"}
/>
<List.Item.Detail.Metadata.Label
title="City"
text={customer.billingAddress?.city ?? "—"}
/>
<List.Item.Detail.Metadata.Label
title="State"
text={customer.billingAddress?.state ?? "—"}
/>
<List.Item.Detail.Metadata.Label
title="Country"
text={customer.billingAddress?.country ?? "—"}
/>
</List.Item.Detail.Metadata>
}
/>
}
actions={
<ActionPanel>
<Action.OpenInBrowser
title="Email Customer"
url={`mailto:${customer.email}`}
/>
<Action.CopyToClipboard
title="Copy Customer ID"
content={customer.id}
/>
</ActionPanel>
}
accessories={[
{
text: organization?.name,
},
]}
/>
);
};

const CustomersView = () => {
const {
data: customers,
isLoading,
fetchNextPage,
hasNextPage,
} = useCustomers({}, 20);

const handleLoadMore = useCallback(() => {
fetchNextPage();
}, [fetchNextPage]);

return (
<List
isLoading={isLoading}
searchBarPlaceholder="Filter Customers..."
filtering={true}
pagination={{
pageSize: 20,
hasMore: hasNextPage,
onLoadMore: handleLoadMore,
}}
isShowingDetail
>
{customers?.pages
.flatMap((page) => page.result.items)
.map((customer) => (
<CustomerItem key={customer.id} customer={customer} />
))}
</List>
);
};
27 changes: 27 additions & 0 deletions extensions/polar/src/hooks/customers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { CustomersListRequest } from "@polar-sh/sdk/models/operations";
import { useInfiniteQuery } from "@tanstack/react-query";
import { useContext } from "react";
import { PolarContext } from "../providers";

export const useCustomers = (
parameters: CustomersListRequest,
limit: number,
) => {
const polar = useContext(PolarContext);

return useInfiniteQuery({
queryKey: ["customers", parameters],
queryFn: ({ pageParam = 1 }) =>
polar.customers.list({ ...parameters, page: pageParam, limit: limit }),
initialPageParam: 1,
getNextPageParam: (lastPage, pages) => {
const currentPage = pages.length;
const totalPages = Math.ceil(
lastPage.result.pagination.totalCount / limit,
);
const nextPage = totalPages > currentPage ? currentPage + 1 : undefined;

return nextPage;
},
});
};
8 changes: 5 additions & 3 deletions extensions/polar/src/oauth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ import fetch from "node-fetch";

const CLIENT_ID = "polar_ci_emNfLiLOhk0njeLomDs14g";

const SCOPES =
"openid profile email user:read organizations:read organizations:write products:read products:write benefits:read benefits:write subscriptions:read subscriptions:write orders:read metrics:read customers:read customers:write";

async function fetchTokens(
authRequest: OAuth.AuthorizationRequest,
authCode: string,
Expand Down Expand Up @@ -58,13 +61,12 @@ export const authenticate = async (): Promise<string> => {
const authRequest = await client.authorizationRequest({
endpoint: "https://polar.sh/oauth2/authorize",
clientId: CLIENT_ID,
scope:
"openid profile email user:read organizations:read organizations:write products:read products:write benefits:read benefits:write subscriptions:read subscriptions:write orders:read metrics:read",
scope: SCOPES,
});

const tokenSet = await client.getTokens();

if (tokenSet?.accessToken) {
if (tokenSet?.accessToken && tokenSet.scope === SCOPES) {
if (tokenSet.refreshToken && tokenSet.isExpired()) {
const tokenResponse = await refreshTokens(tokenSet.refreshToken);
await client.setTokens(tokenResponse);
Expand Down
Loading