Skip to content

Commit 6722b3d

Browse files
committed
fix
1 parent 4fe0df1 commit 6722b3d

File tree

10 files changed

+465
-7
lines changed

10 files changed

+465
-7
lines changed

package.json

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,10 @@
1313
"test": "prettier --check . && xo && ava",
1414
"check": "biome check --write ./src"
1515
},
16-
"files": ["dist", "bin"],
16+
"files": [
17+
"dist",
18+
"bin"
19+
],
1720
"dependencies": {
1821
"@inkjs/ui": "^2.0.0",
1922
"@lemonsqueezy/lemonsqueezy.js": "^4.0.0",
@@ -24,6 +27,7 @@
2427
"ink-link": "^4.1.0",
2528
"listr": "^0.14.3",
2629
"meow": "^11.0.0",
30+
"mime-types": "^2.1.35",
2731
"open": "^10.1.0",
2832
"prompts": "^2.4.2",
2933
"react": "^18.2.0",
@@ -35,6 +39,7 @@
3539
"@biomejs/biome": "1.9.4",
3640
"@sindresorhus/tsconfig": "^3.0.1",
3741
"@types/listr": "^0.14.9",
42+
"@types/mime-types": "^2.1.4",
3843
"@types/prompts": "^2.4.9",
3944
"@types/react": "^18.3.11",
4045
"@vdemedes/prettier-config": "^2.0.1",
@@ -54,7 +59,9 @@
5459
"ts": "module",
5560
"tsx": "module"
5661
},
57-
"nodeArguments": ["--loader=ts-node/esm"]
62+
"nodeArguments": [
63+
"--loader=ts-node/esm"
64+
]
5865
},
5966
"xo": {
6067
"extends": "xo-react",

pnpm-lock.yaml

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/cli.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,4 +103,6 @@ meow(
103103
organization.slug
104104
}/products`,
105105
);
106+
107+
process.exit(0);
106108
})();

src/oauth.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ const config = {
2828
"products:write",
2929
"benefits:read",
3030
"benefits:write",
31+
"files:read",
32+
"files:write",
33+
"discounts:read",
34+
"discounts:write",
3135
],
3236
redirectUrl: "http://127.0.0.1:3333/oauth/callback",
3337
};

src/organization.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,11 @@ export const resolveOrganization = async (
1010
storeSlug: string,
1111
): Promise<Organization> => {
1212
// Get list of organizations user is member of
13-
const userOrganizations = (await api.organizations.list({})).result.items;
13+
const userOrganizations = (
14+
await api.organizations.list({
15+
limit: 100,
16+
})
17+
).result.items;
1418

1519
// If user has organizations, prompt them to select one
1620
const organization = await selectOrganizationPrompt(userOrganizations);

src/product.ts

Lines changed: 180 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
1-
import type { ListProducts, ListVariants } from "@lemonsqueezy/lemonsqueezy.js";
1+
import {
2+
listFiles,
3+
type ListProducts,
4+
type ListVariants,
5+
} from "@lemonsqueezy/lemonsqueezy.js";
26
import type { Polar } from "@polar-sh/sdk";
37
import type {
48
BenefitLicenseKeyExpirationProperties,
9+
FileRead,
510
Interval,
611
Organization,
12+
Product,
713
ProductOneTimeCreate,
814
ProductPriceOneTimeCustomCreate,
915
ProductPriceOneTimeFixedCreate,
@@ -13,6 +19,13 @@ import type {
1319
ProductRecurringCreate,
1420
Timeframe,
1521
} from "@polar-sh/sdk/models/components";
22+
import fs from "node:fs";
23+
import path from "node:path";
24+
import os from "node:os";
25+
import mime from "mime-types";
26+
import https from "node:https";
27+
import { Upload } from "./upload.js";
28+
import { uploadFailedMessage, uploadMessage } from "./ui/upload.js";
1629

1730
const resolveInterval = (
1831
interval: ListVariants["data"][number]["attributes"]["interval"],
@@ -168,5 +181,171 @@ export const createProduct = async (
168181
});
169182
}
170183

184+
try {
185+
await handleFiles(api, organization, variant, product);
186+
} catch (e) {
187+
await uploadFailedMessage();
188+
}
189+
171190
return product;
172191
};
192+
193+
const handleFiles = async (
194+
api: Polar,
195+
organization: Organization,
196+
variant: ListVariants["data"][number],
197+
product: Product,
198+
) => {
199+
const files = await listFiles({
200+
filter: {
201+
variantId: variant.id,
202+
},
203+
});
204+
205+
// Group files with same variant id and download them
206+
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "polar-"));
207+
208+
const groupedFiles =
209+
files.data?.data?.reduce<
210+
Record<string, { downloadUrl: string; filePath: string }[]>
211+
>((acc, file) => {
212+
if ("attributes" in file && "variant_id" in file.attributes) {
213+
const filePath = path.join(tempDir, file.attributes.name);
214+
const url = new URL(file.attributes.download_url);
215+
216+
acc[file.attributes.variant_id] = [
217+
...(acc[file.attributes.variant_id] ?? []),
218+
{
219+
downloadUrl: url.toString(),
220+
filePath,
221+
},
222+
];
223+
}
224+
225+
return acc;
226+
}, {}) ?? {};
227+
228+
await Promise.all(
229+
Object.values(groupedFiles)
230+
.flat()
231+
.map((file) => downloadFile(file.downloadUrl, file.filePath)),
232+
);
233+
234+
// Create one benefit per variant, upload the files to the benefit, and add the benefit to the product
235+
236+
for (const [_, files] of Object.entries(groupedFiles)) {
237+
const fileUploads = await Promise.all(
238+
files.map((file) => uploadFile(api, organization, file.filePath)),
239+
);
240+
241+
const benefit = await api.benefits.create({
242+
type: "downloadables",
243+
description: product.name,
244+
properties: {
245+
files: fileUploads.map((file) => file.id),
246+
},
247+
organizationId: organization.id,
248+
});
249+
250+
await api.products.updateBenefits({
251+
id: product.id,
252+
productBenefitsUpdate: {
253+
benefits: [benefit.id],
254+
},
255+
});
256+
}
257+
258+
// Clean up temporary files
259+
await Promise.all(
260+
Object.values(groupedFiles)
261+
.flat()
262+
.map((file) => fs.promises.unlink(file.filePath)),
263+
);
264+
265+
await fs.promises.rmdir(tempDir);
266+
};
267+
268+
const downloadFile = (url: string, filePath: string) => {
269+
return new Promise<void>((resolve, reject) => {
270+
const options = {
271+
method: "GET",
272+
headers: {
273+
"Content-Type": "application/octet-stream",
274+
},
275+
};
276+
277+
const writer = fs.createWriteStream(filePath);
278+
279+
const request = https.get(url, options, (response) => {
280+
if (response.statusCode !== 200) {
281+
fs.unlink(filePath, (e) => {
282+
if (e) {
283+
console.error(e);
284+
}
285+
});
286+
reject(response);
287+
return;
288+
}
289+
290+
response.pipe(writer);
291+
292+
writer.on("finish", () => {
293+
writer.close();
294+
resolve();
295+
});
296+
});
297+
298+
request.on("error", (err) => {
299+
console.error(err);
300+
301+
fs.unlink(filePath, (e) => {
302+
if (e) {
303+
console.error(e);
304+
}
305+
});
306+
});
307+
308+
writer.on("error", (err) => {
309+
console.error(err);
310+
311+
fs.unlink(filePath, (e) => {
312+
if (e) {
313+
console.error(e);
314+
}
315+
});
316+
});
317+
318+
request.end();
319+
});
320+
};
321+
322+
const uploadFile = async (
323+
api: Polar,
324+
organization: Organization,
325+
filePath: string,
326+
) => {
327+
const readStream = fs.createReadStream(filePath);
328+
const mimeType = mime.lookup(filePath) || "application/octet-stream";
329+
330+
const fileUploadPromise = new Promise<FileRead>((resolve) => {
331+
const upload = new Upload(api, {
332+
organization,
333+
file: {
334+
name: path.basename(filePath),
335+
type: mimeType,
336+
size: fs.statSync(filePath).size,
337+
readStream,
338+
},
339+
onFileUploadProgress: () => {},
340+
onFileUploaded: resolve,
341+
});
342+
343+
upload.run();
344+
});
345+
346+
await uploadMessage(fileUploadPromise);
347+
348+
const fileUpload = await fileUploadPromise;
349+
350+
return fileUpload;
351+
};

src/prompts/store.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export const storePrompt = async (stores: ListStores["data"]) => {
2121
}
2222

2323
if (selectedStore.attributes.currency !== "USD") {
24-
throw new Error("Store currency is not USD");
24+
throw new Error("Store Currency must be USD");
2525
}
2626

2727
return selectedStore;

src/ui/success.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ export const successMessage = async (
99
products: Product[],
1010
server: "sandbox" | "production",
1111
) => {
12-
const { unmount, clear, waitUntilExit } = render(
12+
const { unmount, waitUntilExit } = render(
1313
<Box flexDirection="column" columnGap={2}>
1414
<StatusMessage variant="success">
1515
<Text>Polar was successfully initialized!</Text>
@@ -31,7 +31,6 @@ export const successMessage = async (
3131
);
3232

3333
setTimeout(() => {
34-
clear();
3534
unmount();
3635
}, 1500);
3736

src/ui/upload.tsx

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { Spinner, StatusMessage } from "@inkjs/ui";
2+
import { Text, render } from "ink";
3+
import React from "react";
4+
5+
export const uploadMessage = async <T,>(fileUploadPromise: Promise<T>) => {
6+
const { unmount, clear, waitUntilExit } = render(
7+
<Spinner label="Uploading file..." />,
8+
);
9+
10+
fileUploadPromise.then(() => {
11+
clear();
12+
unmount();
13+
});
14+
15+
await waitUntilExit();
16+
};
17+
18+
export const uploadFailedMessage = async () => {
19+
const { unmount, waitUntilExit } = render(
20+
<StatusMessage variant="warning">
21+
<Text>Could not upload files associated with product</Text>
22+
</StatusMessage>,
23+
);
24+
25+
setTimeout(() => {
26+
unmount();
27+
}, 1000);
28+
29+
await waitUntilExit();
30+
};

0 commit comments

Comments
 (0)