@applaunchflow/mcp 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -8
- package/build/client/api.js +0 -1
- package/build/index.js +3 -3
- package/build/tools/layouts.js +2 -1
- package/build/tools/screenshots.js +21 -1
- package/build/tools/templates.js +0 -36
- package/build/tools/variants.js +0 -28
- package/package.json +5 -1
- package/build/tools/analysis.js +0 -23
- package/build/tools/aso.js +0 -84
- package/build/tools/graphics.js +0 -69
- package/build/tools/store-metadata.js +0 -66
package/README.md
CHANGED
|
@@ -1,11 +1,26 @@
|
|
|
1
|
-
#
|
|
1
|
+
# AppLaunchFlow MCP
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
MCP server for AppLaunchFlow — create App Store & Google Play screenshots with AI.
|
|
4
|
+
|
|
5
|
+
## Setup
|
|
6
|
+
|
|
7
|
+
Add to your MCP client config (e.g. Claude Desktop):
|
|
8
|
+
|
|
9
|
+
```json
|
|
10
|
+
{
|
|
11
|
+
"mcpServers": {
|
|
12
|
+
"applaunchflow": {
|
|
13
|
+
"command": "npx",
|
|
14
|
+
"args": ["-y", "@applaunchflow/mcp@latest"]
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
```
|
|
4
19
|
|
|
5
20
|
## Auth
|
|
6
21
|
|
|
7
22
|
```bash
|
|
8
|
-
|
|
23
|
+
npx -y @applaunchflow/mcp@latest auth login
|
|
9
24
|
```
|
|
10
25
|
|
|
11
26
|
Credentials are stored in `~/.applaunchflow/credentials.json`.
|
|
@@ -16,11 +31,9 @@ Environment overrides:
|
|
|
16
31
|
- `applaunchflow_MCP_TOKEN`
|
|
17
32
|
- `applaunchflow_MCP_COOKIE_NAME`
|
|
18
33
|
|
|
19
|
-
##
|
|
34
|
+
## Development
|
|
20
35
|
|
|
21
36
|
```bash
|
|
22
|
-
pnpm
|
|
23
|
-
|
|
37
|
+
pnpm install
|
|
38
|
+
pnpm dev
|
|
24
39
|
```
|
|
25
|
-
|
|
26
|
-
The server uses stdio transport and exposes tools/resources for project, screenshot, layout, template, variant, graphics, and ASO workflows.
|
package/build/client/api.js
CHANGED
|
@@ -34,7 +34,6 @@ export class AppLaunchFlowClient {
|
|
|
34
34
|
buildHeaders(extraHeaders) {
|
|
35
35
|
const headers = new Headers(extraHeaders);
|
|
36
36
|
headers.set("Cookie", `${this.credentials.cookieName}=${this.credentials.token}`);
|
|
37
|
-
headers.set("Authorization", `Bearer ${this.credentials.token}`);
|
|
38
37
|
return headers;
|
|
39
38
|
}
|
|
40
39
|
async requestJson(path, options = {}) {
|
package/build/index.js
CHANGED
|
@@ -17,7 +17,7 @@ import { registerVariantTools } from "./tools/variants.js";
|
|
|
17
17
|
const SERVER_INSTRUCTIONS = `
|
|
18
18
|
AppLaunchFlow MCP is screenshot-focused at this stage.
|
|
19
19
|
Only use it for screenshot project setup, screenshot uploads, screenshot template generation, variants, and direct screenshot layout editing.
|
|
20
|
-
Do not treat this MCP as an ASO, localization,
|
|
20
|
+
Do not treat this MCP as an ASO, localization, or graphics assistant.
|
|
21
21
|
|
|
22
22
|
Use AppLaunchFlow MCP as an execution tool, not a questionnaire.
|
|
23
23
|
|
|
@@ -39,8 +39,8 @@ Preferred workflows:
|
|
|
39
39
|
- When adding or editing elements, ensure text and screenshots do not overlap. Verify that positions place elements in distinct, non-conflicting areas of the canvas.
|
|
40
40
|
- After composition-sensitive edits, inspect the returned translation or re-fetch the layout before reporting success. If elements overlap or are poorly positioned, fix them before telling the user the edit is done.
|
|
41
41
|
- ALWAYS use browse_templates when a template choice is needed. Never offer templates via text bullet points or AskUserQuestion. The gallery opens in the browser and returns the user's selection automatically.
|
|
42
|
-
-
|
|
43
|
-
-
|
|
42
|
+
- When you need visual context about a screenshot (e.g. to extract colors, understand the app UI, or make context-specific edits), use view_screenshot to look at the actual image.
|
|
43
|
+
- After generating a new variant, always include the editor URL in the reply so the user can open it directly.
|
|
44
44
|
|
|
45
45
|
Translation and localization:
|
|
46
46
|
- When the user asks to translate, localize, or create a version in another language, ALWAYS use translate_layouts. Do NOT manually edit text nodes via transform_layout for translation.
|
package/build/tools/layouts.js
CHANGED
|
@@ -117,7 +117,8 @@ export function registerLayoutTools(server, client) {
|
|
|
117
117
|
"4. Dot-notation is supported for deep updates without replacing the whole object. Example: {'richContent.attrs.defaultFontSize': 96} updates only the font size inside richContent.attrs, preserving all other fields. Always use dot-notation for nested property changes. " +
|
|
118
118
|
"5. For add_node, changes MUST include an 'id' field. " +
|
|
119
119
|
"6. To add new screens, first add empty screen containers, then populate them in a SECOND call using selector 'screenId:<id>'. " +
|
|
120
|
-
"7. Default to layouts:['mobile']. Only include tablet/desktop if the user asks."
|
|
120
|
+
"7. Default to layouts:['mobile']. Only include tablet/desktop if the user asks. " +
|
|
121
|
+
"8. FONT SIZE: The rendered font size is controlled ONLY by 'richContent.attrs.defaultFontSize' (pixel value). To change font size, use dot-notation: {'richContent.attrs.defaultFontSize': 80}. Do NOT use 'fontSizeScale' — that property is for promo videos only and has NO effect on screenshot rendering.",
|
|
121
122
|
inputSchema: {
|
|
122
123
|
generationId: z.string().uuid(),
|
|
123
124
|
language: z.string(),
|
|
@@ -43,7 +43,27 @@ export function registerScreenshotTools(server, client) {
|
|
|
43
43
|
},
|
|
44
44
|
}, async (args) => {
|
|
45
45
|
try {
|
|
46
|
-
|
|
46
|
+
const result = await client.generateLayouts(args);
|
|
47
|
+
const generationId = args.generationId || args.projectId || "";
|
|
48
|
+
const variantId = result.variantId || "";
|
|
49
|
+
const editorUrl = `${client.credentials.baseUrl}/editor?projectId=${generationId}&variantId=${variantId}&device=phone`;
|
|
50
|
+
return {
|
|
51
|
+
content: [
|
|
52
|
+
{
|
|
53
|
+
type: "text",
|
|
54
|
+
text: [
|
|
55
|
+
"Generated layouts successfully.",
|
|
56
|
+
`Editor URL: ${editorUrl}`,
|
|
57
|
+
"IMPORTANT: Paste this exact editor URL in the reply so the user can open it.",
|
|
58
|
+
].join("\n"),
|
|
59
|
+
},
|
|
60
|
+
],
|
|
61
|
+
structuredContent: {
|
|
62
|
+
success: true,
|
|
63
|
+
data: { ...result, editorUrl },
|
|
64
|
+
message: "Generated layouts",
|
|
65
|
+
},
|
|
66
|
+
};
|
|
47
67
|
}
|
|
48
68
|
catch (error) {
|
|
49
69
|
return fail(error);
|
package/build/tools/templates.js
CHANGED
|
@@ -291,40 +291,4 @@ export function registerTemplateTools(server, client, selectionCoordinator) {
|
|
|
291
291
|
return fail(error);
|
|
292
292
|
}
|
|
293
293
|
});
|
|
294
|
-
server.registerTool("list_templates", {
|
|
295
|
-
title: "List Templates",
|
|
296
|
-
description: "List all AppLaunchFlow screenshot templates with visual preview resources. Prefer visual comparison over text-only descriptions.",
|
|
297
|
-
inputSchema: {
|
|
298
|
-
deviceType: z
|
|
299
|
-
.enum(TEMPLATE_PREVIEW_DEVICE_TYPES)
|
|
300
|
-
.optional()
|
|
301
|
-
.describe("Which preview device to attach for each template."),
|
|
302
|
-
},
|
|
303
|
-
}, async ({ deviceType = "phone" }) => {
|
|
304
|
-
try {
|
|
305
|
-
const payload = decorateTemplatePayload(await client.listTemplates(), client.credentials.baseUrl);
|
|
306
|
-
const galleryUrl = buildTemplateGalleryUrl(client.credentials.baseUrl, {
|
|
307
|
-
deviceType,
|
|
308
|
-
});
|
|
309
|
-
return buildListTemplatesResult(payload, deviceType, galleryUrl);
|
|
310
|
-
}
|
|
311
|
-
catch (error) {
|
|
312
|
-
return fail(error);
|
|
313
|
-
}
|
|
314
|
-
});
|
|
315
|
-
server.registerTool("get_template_details", {
|
|
316
|
-
title: "Get Template Details",
|
|
317
|
-
description: "Get details and visual preview resources for a single screenshot template.",
|
|
318
|
-
inputSchema: {
|
|
319
|
-
templateId: z.string(),
|
|
320
|
-
},
|
|
321
|
-
}, async ({ templateId }) => {
|
|
322
|
-
try {
|
|
323
|
-
const payload = decorateTemplatePayload(await client.getTemplate(templateId), client.credentials.baseUrl);
|
|
324
|
-
return buildTemplateDetailsResult(payload);
|
|
325
|
-
}
|
|
326
|
-
catch (error) {
|
|
327
|
-
return fail(error);
|
|
328
|
-
}
|
|
329
|
-
});
|
|
330
294
|
}
|
package/build/tools/variants.js
CHANGED
|
@@ -53,20 +53,6 @@ export function registerVariantTools(server, client) {
|
|
|
53
53
|
return fail(error);
|
|
54
54
|
}
|
|
55
55
|
});
|
|
56
|
-
server.registerTool("switch_variant", {
|
|
57
|
-
title: "Switch Variant",
|
|
58
|
-
description: "Set a variant as active",
|
|
59
|
-
inputSchema: {
|
|
60
|
-
variantId: z.string().uuid(),
|
|
61
|
-
},
|
|
62
|
-
}, async ({ variantId }) => {
|
|
63
|
-
try {
|
|
64
|
-
return ok(await client.switchVariant(variantId), "Switched variant");
|
|
65
|
-
}
|
|
66
|
-
catch (error) {
|
|
67
|
-
return fail(error);
|
|
68
|
-
}
|
|
69
|
-
});
|
|
70
56
|
server.registerTool("duplicate_variant", {
|
|
71
57
|
title: "Duplicate Variant",
|
|
72
58
|
description: "Duplicate an existing variant",
|
|
@@ -81,18 +67,4 @@ export function registerVariantTools(server, client) {
|
|
|
81
67
|
return fail(error);
|
|
82
68
|
}
|
|
83
69
|
});
|
|
84
|
-
server.registerTool("delete_variant", {
|
|
85
|
-
title: "Delete Variant",
|
|
86
|
-
description: "Delete a content variant",
|
|
87
|
-
inputSchema: {
|
|
88
|
-
variantId: z.string().uuid(),
|
|
89
|
-
},
|
|
90
|
-
}, async ({ variantId }) => {
|
|
91
|
-
try {
|
|
92
|
-
return ok(await client.deleteVariant(variantId), "Deleted variant");
|
|
93
|
-
}
|
|
94
|
-
catch (error) {
|
|
95
|
-
return fail(error);
|
|
96
|
-
}
|
|
97
|
-
});
|
|
98
70
|
}
|
package/package.json
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@applaunchflow/mcp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "MCP server for AppLaunchFlow — create App Store & Google Play screenshots with AI.",
|
|
5
5
|
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/ynnickw/applaunchflow-mcp"
|
|
9
|
+
},
|
|
6
10
|
"type": "module",
|
|
7
11
|
"bin": {
|
|
8
12
|
"applaunchflow-mcp": "./build/index.js"
|
package/build/tools/analysis.js
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
import { fail, ok } from "./utils.js";
|
|
3
|
-
import { lookupStoreMetadata } from "./store-metadata.js";
|
|
4
|
-
export function registerAnalysisTools(server, client) {
|
|
5
|
-
server.registerTool("analyze_app", {
|
|
6
|
-
title: "Analyze App",
|
|
7
|
-
description: "Analyze an App Store or Google Play URL and return app metadata",
|
|
8
|
-
inputSchema: {
|
|
9
|
-
app: z.string().min(1),
|
|
10
|
-
country: z.string().length(2).optional(),
|
|
11
|
-
},
|
|
12
|
-
}, async ({ app, country }) => {
|
|
13
|
-
try {
|
|
14
|
-
const data = await lookupStoreMetadata(client, app, country || "us");
|
|
15
|
-
return ok(data, data.provider === "google_play"
|
|
16
|
-
? "Fetched Google Play metadata"
|
|
17
|
-
: "Fetched App Store metadata");
|
|
18
|
-
}
|
|
19
|
-
catch (error) {
|
|
20
|
-
return fail(error);
|
|
21
|
-
}
|
|
22
|
-
});
|
|
23
|
-
}
|
package/build/tools/aso.js
DELETED
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
import { fail, ok } from "./utils.js";
|
|
3
|
-
export function registerAsoTools(server, client) {
|
|
4
|
-
server.registerTool("generate_aso_copy", {
|
|
5
|
-
title: "Generate ASO Copy",
|
|
6
|
-
description: "Generate ASO copy for a project",
|
|
7
|
-
inputSchema: {
|
|
8
|
-
generationId: z.string().uuid(),
|
|
9
|
-
variantId: z.string().uuid().optional(),
|
|
10
|
-
metadata: z.record(z.any()).optional(),
|
|
11
|
-
competitorAppIds: z.array(z.number()).optional(),
|
|
12
|
-
},
|
|
13
|
-
}, async (args) => {
|
|
14
|
-
try {
|
|
15
|
-
return ok(await client.generateAsoCopy(args), "Generated ASO copy");
|
|
16
|
-
}
|
|
17
|
-
catch (error) {
|
|
18
|
-
return fail(error);
|
|
19
|
-
}
|
|
20
|
-
});
|
|
21
|
-
server.registerTool("get_aso_copy", {
|
|
22
|
-
title: "Get ASO Copy",
|
|
23
|
-
description: "Get ASO copy for a project",
|
|
24
|
-
inputSchema: {
|
|
25
|
-
generationId: z.string().uuid(),
|
|
26
|
-
variantId: z.string().uuid().optional(),
|
|
27
|
-
},
|
|
28
|
-
}, async ({ generationId, variantId }) => {
|
|
29
|
-
try {
|
|
30
|
-
return ok(await client.getAsoCopy(generationId, variantId), "Fetched ASO copy");
|
|
31
|
-
}
|
|
32
|
-
catch (error) {
|
|
33
|
-
return fail(error);
|
|
34
|
-
}
|
|
35
|
-
});
|
|
36
|
-
server.registerTool("update_aso_copy", {
|
|
37
|
-
title: "Update ASO Copy",
|
|
38
|
-
description: "Persist edited ASO copy",
|
|
39
|
-
inputSchema: {
|
|
40
|
-
generationId: z.string().uuid(),
|
|
41
|
-
variantId: z.string().uuid().optional(),
|
|
42
|
-
asoCopy: z.record(z.any()),
|
|
43
|
-
},
|
|
44
|
-
}, async (args) => {
|
|
45
|
-
try {
|
|
46
|
-
return ok(await client.updateAsoCopy(args), "Updated ASO copy");
|
|
47
|
-
}
|
|
48
|
-
catch (error) {
|
|
49
|
-
return fail(error);
|
|
50
|
-
}
|
|
51
|
-
});
|
|
52
|
-
server.registerTool("translate_aso_copy", {
|
|
53
|
-
title: "Translate ASO Copy",
|
|
54
|
-
description: "Translate ASO copy into target languages",
|
|
55
|
-
inputSchema: {
|
|
56
|
-
generationId: z.string().uuid(),
|
|
57
|
-
variantId: z.string().uuid().optional(),
|
|
58
|
-
targetLanguages: z.array(z.string()).min(1),
|
|
59
|
-
},
|
|
60
|
-
}, async (args) => {
|
|
61
|
-
try {
|
|
62
|
-
return ok(await client.translateAsoCopy(args), "Translated ASO copy");
|
|
63
|
-
}
|
|
64
|
-
catch (error) {
|
|
65
|
-
return fail(error);
|
|
66
|
-
}
|
|
67
|
-
});
|
|
68
|
-
server.registerTool("suggest_competitors", {
|
|
69
|
-
title: "Suggest Competitors",
|
|
70
|
-
description: "Suggest competitor apps for ASO analysis",
|
|
71
|
-
inputSchema: {
|
|
72
|
-
appName: z.string(),
|
|
73
|
-
category: z.string(),
|
|
74
|
-
platform: z.enum(["ios", "android", "both"]).optional(),
|
|
75
|
-
},
|
|
76
|
-
}, async (args) => {
|
|
77
|
-
try {
|
|
78
|
-
return ok(await client.suggestCompetitors(args), "Fetched competitors");
|
|
79
|
-
}
|
|
80
|
-
catch (error) {
|
|
81
|
-
return fail(error);
|
|
82
|
-
}
|
|
83
|
-
});
|
|
84
|
-
}
|
package/build/tools/graphics.js
DELETED
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
import { fail, ok } from "./utils.js";
|
|
3
|
-
export function registerGraphicsTools(server, client) {
|
|
4
|
-
server.registerTool("generate_graphics", {
|
|
5
|
-
title: "Generate Graphics",
|
|
6
|
-
description: "Generate social graphics suggestions for a project",
|
|
7
|
-
inputSchema: {
|
|
8
|
-
generationId: z.string().uuid(),
|
|
9
|
-
templateId: z.string(),
|
|
10
|
-
},
|
|
11
|
-
}, async (args) => {
|
|
12
|
-
try {
|
|
13
|
-
return ok(await client.generateGraphics(args), "Generated graphics");
|
|
14
|
-
}
|
|
15
|
-
catch (error) {
|
|
16
|
-
return fail(error);
|
|
17
|
-
}
|
|
18
|
-
});
|
|
19
|
-
server.registerTool("get_graphics", {
|
|
20
|
-
title: "Get Graphics",
|
|
21
|
-
description: "Get saved social graphics for a project",
|
|
22
|
-
inputSchema: {
|
|
23
|
-
projectId: z.string().uuid(),
|
|
24
|
-
variantId: z.string().uuid().optional(),
|
|
25
|
-
},
|
|
26
|
-
}, async ({ projectId, variantId }) => {
|
|
27
|
-
try {
|
|
28
|
-
return ok(await client.getGraphics(projectId, variantId), "Fetched graphics");
|
|
29
|
-
}
|
|
30
|
-
catch (error) {
|
|
31
|
-
return fail(error);
|
|
32
|
-
}
|
|
33
|
-
});
|
|
34
|
-
server.registerTool("save_graphics", {
|
|
35
|
-
title: "Save Graphics",
|
|
36
|
-
description: "Persist social graphics layouts for a project",
|
|
37
|
-
inputSchema: {
|
|
38
|
-
generationId: z.string().uuid(),
|
|
39
|
-
variantId: z.string().uuid().optional(),
|
|
40
|
-
socialTemplateId: z.string(),
|
|
41
|
-
socialPrimaryFormat: z.enum([
|
|
42
|
-
"og",
|
|
43
|
-
"x_post",
|
|
44
|
-
"play_store_feature",
|
|
45
|
-
"x_header",
|
|
46
|
-
"linkedin_banner",
|
|
47
|
-
]),
|
|
48
|
-
graphics: z
|
|
49
|
-
.array(z.object({
|
|
50
|
-
format: z.enum([
|
|
51
|
-
"og",
|
|
52
|
-
"x_post",
|
|
53
|
-
"play_store_feature",
|
|
54
|
-
"x_header",
|
|
55
|
-
"linkedin_banner",
|
|
56
|
-
]),
|
|
57
|
-
layout: z.record(z.any()),
|
|
58
|
-
}))
|
|
59
|
-
.min(1),
|
|
60
|
-
},
|
|
61
|
-
}, async (args) => {
|
|
62
|
-
try {
|
|
63
|
-
return ok(await client.saveGraphics(args), "Saved graphics");
|
|
64
|
-
}
|
|
65
|
-
catch (error) {
|
|
66
|
-
return fail(error);
|
|
67
|
-
}
|
|
68
|
-
});
|
|
69
|
-
}
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
export function extractAppStoreId(input) {
|
|
2
|
-
if (/^\d+$/.test(input)) {
|
|
3
|
-
return input;
|
|
4
|
-
}
|
|
5
|
-
const match = input.match(/id(\d+)/);
|
|
6
|
-
return match ? match[1] : null;
|
|
7
|
-
}
|
|
8
|
-
export async function fetchGooglePlayMetadata(urlString) {
|
|
9
|
-
const url = new URL(urlString);
|
|
10
|
-
const packageName = url.searchParams.get("id");
|
|
11
|
-
if (!packageName) {
|
|
12
|
-
throw new Error("Google Play URL must include an id query parameter");
|
|
13
|
-
}
|
|
14
|
-
const response = await fetch(url.toString(), {
|
|
15
|
-
headers: {
|
|
16
|
-
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
|
|
17
|
-
"Accept-Language": "en-US,en;q=0.9",
|
|
18
|
-
},
|
|
19
|
-
});
|
|
20
|
-
if (!response.ok) {
|
|
21
|
-
throw new Error(`Google Play request failed with status ${response.status}`);
|
|
22
|
-
}
|
|
23
|
-
const html = await response.text();
|
|
24
|
-
const scriptMatch = html.match(/<script type="application\/ld\+json">([\s\S]*?)<\/script>/);
|
|
25
|
-
if (!scriptMatch) {
|
|
26
|
-
throw new Error("Unable to parse Google Play metadata");
|
|
27
|
-
}
|
|
28
|
-
const payload = JSON.parse(scriptMatch[1]);
|
|
29
|
-
return {
|
|
30
|
-
provider: "google_play",
|
|
31
|
-
packageName,
|
|
32
|
-
appName: payload.name || null,
|
|
33
|
-
description: payload.description || null,
|
|
34
|
-
category: payload.applicationCategory || null,
|
|
35
|
-
iconUrl: payload.image || null,
|
|
36
|
-
rating: payload.aggregateRating?.ratingValue || null,
|
|
37
|
-
ratingCount: payload.aggregateRating?.ratingCount || null,
|
|
38
|
-
url: url.toString(),
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
export async function lookupStoreMetadata(client, app, country = "us") {
|
|
42
|
-
if (app.includes("play.google.com")) {
|
|
43
|
-
return fetchGooglePlayMetadata(app);
|
|
44
|
-
}
|
|
45
|
-
const appId = extractAppStoreId(app);
|
|
46
|
-
if (!appId) {
|
|
47
|
-
throw new Error("Provide an App Store URL, Google Play URL, or App Store numeric id");
|
|
48
|
-
}
|
|
49
|
-
const lookup = await client.lookupAppStore(appId, country);
|
|
50
|
-
const appResult = lookup.results?.[0];
|
|
51
|
-
if (!appResult) {
|
|
52
|
-
throw new Error("App Store app not found");
|
|
53
|
-
}
|
|
54
|
-
return {
|
|
55
|
-
provider: "apple",
|
|
56
|
-
appleAppId: appId,
|
|
57
|
-
appName: appResult.trackName || null,
|
|
58
|
-
description: appResult.description || null,
|
|
59
|
-
iconUrl: appResult.artworkUrl512 || appResult.artworkUrl100 || null,
|
|
60
|
-
category: appResult.primaryGenreName || null,
|
|
61
|
-
screenshotUrls: appResult.screenshotUrls || [],
|
|
62
|
-
averageUserRating: appResult.averageUserRating || null,
|
|
63
|
-
userRatingCount: appResult.userRatingCount || null,
|
|
64
|
-
appStoreUrl: appResult.trackViewUrl || null,
|
|
65
|
-
};
|
|
66
|
-
}
|