Skip to content

Commit

Permalink
fix
Browse files Browse the repository at this point in the history
  • Loading branch information
emilwidlund committed Nov 22, 2024
1 parent 4fe0df1 commit 6722b3d
Show file tree
Hide file tree
Showing 10 changed files with 465 additions and 7 deletions.
11 changes: 9 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@
"test": "prettier --check . && xo && ava",
"check": "biome check --write ./src"
},
"files": ["dist", "bin"],
"files": [
"dist",
"bin"
],
"dependencies": {
"@inkjs/ui": "^2.0.0",
"@lemonsqueezy/lemonsqueezy.js": "^4.0.0",
Expand All @@ -24,6 +27,7 @@
"ink-link": "^4.1.0",
"listr": "^0.14.3",
"meow": "^11.0.0",
"mime-types": "^2.1.35",
"open": "^10.1.0",
"prompts": "^2.4.2",
"react": "^18.2.0",
Expand All @@ -35,6 +39,7 @@
"@biomejs/biome": "1.9.4",
"@sindresorhus/tsconfig": "^3.0.1",
"@types/listr": "^0.14.9",
"@types/mime-types": "^2.1.4",
"@types/prompts": "^2.4.9",
"@types/react": "^18.3.11",
"@vdemedes/prettier-config": "^2.0.1",
Expand All @@ -54,7 +59,9 @@
"ts": "module",
"tsx": "module"
},
"nodeArguments": ["--loader=ts-node/esm"]
"nodeArguments": [
"--loader=ts-node/esm"
]
},
"xo": {
"extends": "xo-react",
Expand Down
11 changes: 11 additions & 0 deletions pnpm-lock.yaml

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

2 changes: 2 additions & 0 deletions src/cli.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -103,4 +103,6 @@ meow(
organization.slug
}/products`,
);

process.exit(0);
})();
4 changes: 4 additions & 0 deletions src/oauth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ const config = {
"products:write",
"benefits:read",
"benefits:write",
"files:read",
"files:write",
"discounts:read",
"discounts:write",
],
redirectUrl: "http://127.0.0.1:3333/oauth/callback",
};
Expand Down
6 changes: 5 additions & 1 deletion src/organization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@ export const resolveOrganization = async (
storeSlug: string,
): Promise<Organization> => {
// Get list of organizations user is member of
const userOrganizations = (await api.organizations.list({})).result.items;
const userOrganizations = (
await api.organizations.list({
limit: 100,
})
).result.items;

// If user has organizations, prompt them to select one
const organization = await selectOrganizationPrompt(userOrganizations);
Expand Down
181 changes: 180 additions & 1 deletion src/product.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
import type { ListProducts, ListVariants } from "@lemonsqueezy/lemonsqueezy.js";
import {
listFiles,
type ListProducts,
type ListVariants,
} from "@lemonsqueezy/lemonsqueezy.js";
import type { Polar } from "@polar-sh/sdk";
import type {
BenefitLicenseKeyExpirationProperties,
FileRead,
Interval,
Organization,
Product,
ProductOneTimeCreate,
ProductPriceOneTimeCustomCreate,
ProductPriceOneTimeFixedCreate,
Expand All @@ -13,6 +19,13 @@ import type {
ProductRecurringCreate,
Timeframe,
} from "@polar-sh/sdk/models/components";
import fs from "node:fs";
import path from "node:path";
import os from "node:os";
import mime from "mime-types";
import https from "node:https";
import { Upload } from "./upload.js";
import { uploadFailedMessage, uploadMessage } from "./ui/upload.js";

const resolveInterval = (
interval: ListVariants["data"][number]["attributes"]["interval"],
Expand Down Expand Up @@ -168,5 +181,171 @@ export const createProduct = async (
});
}

try {
await handleFiles(api, organization, variant, product);
} catch (e) {
await uploadFailedMessage();
}

return product;
};

const handleFiles = async (
api: Polar,
organization: Organization,
variant: ListVariants["data"][number],
product: Product,
) => {
const files = await listFiles({
filter: {
variantId: variant.id,
},
});

// Group files with same variant id and download them
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "polar-"));

const groupedFiles =
files.data?.data?.reduce<
Record<string, { downloadUrl: string; filePath: string }[]>
>((acc, file) => {
if ("attributes" in file && "variant_id" in file.attributes) {
const filePath = path.join(tempDir, file.attributes.name);
const url = new URL(file.attributes.download_url);

acc[file.attributes.variant_id] = [
...(acc[file.attributes.variant_id] ?? []),
{
downloadUrl: url.toString(),
filePath,
},
];
}

return acc;
}, {}) ?? {};

await Promise.all(
Object.values(groupedFiles)
.flat()
.map((file) => downloadFile(file.downloadUrl, file.filePath)),
);

// Create one benefit per variant, upload the files to the benefit, and add the benefit to the product

for (const [_, files] of Object.entries(groupedFiles)) {
const fileUploads = await Promise.all(
files.map((file) => uploadFile(api, organization, file.filePath)),
);

const benefit = await api.benefits.create({
type: "downloadables",
description: product.name,
properties: {
files: fileUploads.map((file) => file.id),
},
organizationId: organization.id,
});

await api.products.updateBenefits({
id: product.id,
productBenefitsUpdate: {
benefits: [benefit.id],
},
});
}

// Clean up temporary files
await Promise.all(
Object.values(groupedFiles)
.flat()
.map((file) => fs.promises.unlink(file.filePath)),
);

await fs.promises.rmdir(tempDir);
};

const downloadFile = (url: string, filePath: string) => {
return new Promise<void>((resolve, reject) => {
const options = {
method: "GET",
headers: {
"Content-Type": "application/octet-stream",
},
};

const writer = fs.createWriteStream(filePath);

const request = https.get(url, options, (response) => {
if (response.statusCode !== 200) {
fs.unlink(filePath, (e) => {
if (e) {
console.error(e);
}
});
reject(response);
return;
}

response.pipe(writer);

writer.on("finish", () => {
writer.close();
resolve();
});
});

request.on("error", (err) => {
console.error(err);

fs.unlink(filePath, (e) => {
if (e) {
console.error(e);
}
});
});

writer.on("error", (err) => {
console.error(err);

fs.unlink(filePath, (e) => {
if (e) {
console.error(e);
}
});
});

request.end();
});
};

const uploadFile = async (
api: Polar,
organization: Organization,
filePath: string,
) => {
const readStream = fs.createReadStream(filePath);
const mimeType = mime.lookup(filePath) || "application/octet-stream";

const fileUploadPromise = new Promise<FileRead>((resolve) => {
const upload = new Upload(api, {
organization,
file: {
name: path.basename(filePath),
type: mimeType,
size: fs.statSync(filePath).size,
readStream,
},
onFileUploadProgress: () => {},
onFileUploaded: resolve,
});

upload.run();
});

await uploadMessage(fileUploadPromise);

const fileUpload = await fileUploadPromise;

return fileUpload;
};
2 changes: 1 addition & 1 deletion src/prompts/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export const storePrompt = async (stores: ListStores["data"]) => {
}

if (selectedStore.attributes.currency !== "USD") {
throw new Error("Store currency is not USD");
throw new Error("Store Currency must be USD");
}

return selectedStore;
Expand Down
3 changes: 1 addition & 2 deletions src/ui/success.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export const successMessage = async (
products: Product[],
server: "sandbox" | "production",
) => {
const { unmount, clear, waitUntilExit } = render(
const { unmount, waitUntilExit } = render(
<Box flexDirection="column" columnGap={2}>
<StatusMessage variant="success">
<Text>Polar was successfully initialized!</Text>
Expand All @@ -31,7 +31,6 @@ export const successMessage = async (
);

setTimeout(() => {
clear();
unmount();
}, 1500);

Expand Down
30 changes: 30 additions & 0 deletions src/ui/upload.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Spinner, StatusMessage } from "@inkjs/ui";
import { Text, render } from "ink";
import React from "react";

export const uploadMessage = async <T,>(fileUploadPromise: Promise<T>) => {
const { unmount, clear, waitUntilExit } = render(
<Spinner label="Uploading file..." />,
);

fileUploadPromise.then(() => {
clear();
unmount();
});

await waitUntilExit();
};

export const uploadFailedMessage = async () => {
const { unmount, waitUntilExit } = render(
<StatusMessage variant="warning">
<Text>Could not upload files associated with product</Text>
</StatusMessage>,
);

setTimeout(() => {
unmount();
}, 1000);

await waitUntilExit();
};
Loading

0 comments on commit 6722b3d

Please sign in to comment.