diff --git a/packages/bbui/src/Form/Core/File.svelte b/packages/bbui/src/Form/Core/File.svelte
index a4bc576e95..1a9014e741 100644
--- a/packages/bbui/src/Form/Core/File.svelte
+++ b/packages/bbui/src/Form/Core/File.svelte
@@ -6,7 +6,10 @@
const BYTES_IN_MB = 1000000
- export let value: File | undefined = undefined
+ export let value:
+ | File
+ | { name: string; type: string; size?: number }
+ | undefined = undefined
export let title: string = "Upload file"
export let disabled: boolean = false
export let allowClear: boolean | undefined = undefined
diff --git a/packages/bbui/src/Form/File.svelte b/packages/bbui/src/Form/File.svelte
index b596b81537..ce7506d829 100644
--- a/packages/bbui/src/Form/File.svelte
+++ b/packages/bbui/src/Form/File.svelte
@@ -12,7 +12,8 @@
export let extensions: string[] | undefined = undefined
export let error: string | undefined = undefined
export let title: string | undefined = undefined
- export let value: File | undefined = undefined
+ export let value: File | { name: string; type: string } | undefined =
+ undefined
export let tooltip: string | undefined = undefined
export let helpText: string | undefined = undefined
diff --git a/packages/builder/src/pages/builder/app/[application]/settings/pwa.svelte b/packages/builder/src/pages/builder/app/[application]/settings/pwa.svelte
index 7b4b20dff8..2d55a0bd5e 100644
--- a/packages/builder/src/pages/builder/app/[application]/settings/pwa.svelte
+++ b/packages/builder/src/pages/builder/app/[application]/settings/pwa.svelte
@@ -25,7 +25,7 @@
let saving = false
let pwaEnabled = true
- let pwaBuilderIcons: any = null
+ let uploadingIcons = false
let pwaConfig = $appStore.pwa || {
name: "",
@@ -39,38 +39,24 @@
start_url: "",
}
- function ensureHexFormat(color: string) {
- if (!color) return "#FFFFFF"
- if (color.startsWith("#")) return color
-
- const rgbMatch = color.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/)
- if (rgbMatch) {
- const r = parseInt(rgbMatch[1]).toString(16).padStart(2, "0")
- const g = parseInt(rgbMatch[2]).toString(16).padStart(2, "0")
- const b = parseInt(rgbMatch[3]).toString(16).padStart(2, "0")
- return `#${r}${g}${b}`.toUpperCase()
- }
-
- const rgbaMatch = color.match(/rgba\((\d+),\s*(\d+),\s*(\d+),\s*[\d.]+\)/)
- if (rgbaMatch) {
- const r = parseInt(rgbaMatch[1]).toString(16).padStart(2, "0")
- const g = parseInt(rgbaMatch[2]).toString(16).padStart(2, "0")
- const b = parseInt(rgbaMatch[3]).toString(16).padStart(2, "0")
- return `#${r}${g}${b}`.toUpperCase()
- }
-
- return "#FFFFFF"
- }
+ $: iconCount = pwaConfig.icons?.length || 0
+ $: iconFileDisplay = iconCount
+ ? {
+ name: `${iconCount} icons uploaded`,
+ type: "file",
+ }
+ : undefined
function getCssVariableValue(cssVar: string) {
try {
if (cssVar?.startsWith("#")) return cssVar
const varMatch = cssVar?.match(/var\((.*?)\)/)
- if (!varMatch) return ensureHexFormat(cssVar)
+ if (!varMatch) return "#FFFFFF"
- const varName = varMatch[1]
- return ensureHexFormat(
+ const varName = varMatch?.[1]
+ return (
+ varName &&
getComputedStyle(document.documentElement)
.getPropertyValue(varName)
.trim()
@@ -82,7 +68,10 @@
}
async function handlePWAZip(file: File) {
+ if (!file) return
+
try {
+ uploadingIcons = true
const data = new FormData()
data.append("file", file as any)
const result = await API.uploadPWAZip(data)
@@ -91,16 +80,14 @@
notifications.success(
`Processed ${pwaConfig.icons.length} icons from PWA Builder`
)
- pwaBuilderIcons = {
- name: file instanceof File ? file.name : "PWA Icons",
- type: "file",
- }
} catch (error: any) {
console.error("Error processing PWA Builder zip:", error)
notifications.error(
"Failed to process PWA Builder zip: " +
(error.message || "Unknown error")
)
+ } finally {
+ uploadingIcons = false
}
}
@@ -208,8 +195,8 @@
notifications.error("File too large. 20mb limit")}
extensions={[".zip"]}
on:change={e => e.detail && handlePWAZip(e.detail)}
- value={pwaBuilderIcons}
- disabled={!pwaEnabled}
+ value={iconFileDisplay}
+ disabled={!pwaEnabled || uploadingIcons}
/>
@@ -257,7 +244,7 @@
-
diff --git a/packages/server/src/api/controllers/static/index.ts b/packages/server/src/api/controllers/static/index.ts
index 8dd13d5d30..b0ebae1a96 100644
--- a/packages/server/src/api/controllers/static/index.ts
+++ b/packages/server/src/api/controllers/static/index.ts
@@ -144,11 +144,14 @@ export const uploadFile = async function (
export async function processPWAZip(ctx: UserCtx) {
const file = ctx.request.files?.file
- console.log("file", file)
if (!file || Array.isArray(file)) {
ctx.throw(400, "No file or multiple files provided")
}
+ if (!file.path || !file.name?.toLowerCase().endsWith(".zip")) {
+ ctx.throw(400, "Invalid file - must be a zip file")
+ }
+
const tempDir = join(tmpdir(), `pwa-${Date.now()}`)
try {
await fs.promises.mkdir(tempDir, { recursive: true })
@@ -156,35 +159,60 @@ export async function processPWAZip(ctx: UserCtx) {
await extract(file.path, { dir: tempDir })
const iconsJsonPath = join(tempDir, "icons.json")
- if (!iconsJsonPath) {
+ if (!fs.existsSync(iconsJsonPath)) {
ctx.throw(400, "Invalid zip structure - missing icons.json")
}
- const iconsContent = await fs.promises.readFile(iconsJsonPath, "utf-8")
- const iconsData = JSON.parse(iconsContent)
+ let iconsData
+ try {
+ const iconsContent = await fs.promises.readFile(iconsJsonPath, "utf-8")
+ iconsData = JSON.parse(iconsContent)
+ } catch (error) {
+ ctx.throw(400, "Invalid icons.json file - could not parse JSON")
+ }
+
+ if (!iconsData.icons || !Array.isArray(iconsData.icons)) {
+ ctx.throw(400, "Invalid icons.json file - missing icons array")
+ }
const icons = []
const baseDir = path.dirname(iconsJsonPath)
+ const appId = context.getProdAppId()
for (const icon of iconsData.icons) {
- const extension = path.extname(icon.src)
- const key = `${context.getProdAppId()}/pwa/${uuid.v4()}${extension}`
+ if (!icon.src || !icon.sizes || !fs.existsSync(join(baseDir, icon.src))) {
+ continue
+ }
- const result = await objectStore.upload({
- bucket: ObjectStoreBuckets.APPS,
- filename: key,
- path: path.join(baseDir, icon.src),
- type: icon.type || "image/png",
- })
+ const extension = path.extname(icon.src) || ".png"
+ const key = `${appId}/pwa/${uuid.v4()}${extension}`
+ const mimeType =
+ icon.type || (extension === ".png" ? "image/png" : "image/jpeg")
- if (result.Key) {
- icons.push({
- src: result.Key,
- sizes: icon.sizes,
- type: icon.type || "image/png",
+ try {
+ const result = await objectStore.upload({
+ bucket: ObjectStoreBuckets.APPS,
+ filename: key,
+ path: join(baseDir, icon.src),
+ type: mimeType,
})
+
+ if (result.Key) {
+ icons.push({
+ src: result.Key,
+ sizes: icon.sizes,
+ type: mimeType,
+ })
+ }
+ } catch (uploadError) {
+ console.error(`Failed to upload icon ${icon.src}:`, uploadError)
}
}
+
+ if (icons.length === 0) {
+ ctx.throw(400, "No valid icons found in the zip file")
+ }
+
ctx.body = { icons }
} catch (error: any) {
ctx.throw(500, `Error processing zip: ${error.message}`)
@@ -521,16 +549,42 @@ export async function serveManifest(ctx: UserCtx) {
background_color: appInfo.pwa.background_color || "#FFFFFF",
theme_color: appInfo.pwa.theme_color || "#FFFFFF",
icons: [],
+ screenshots: [],
}
if (appInfo.pwa.icons && appInfo.pwa.icons.length > 0) {
try {
manifest.icons = await objectStore.enrichPWAImages(appInfo.pwa.icons)
+
+ const desktopScreenshot = manifest.icons.find(
+ icon => icon.sizes === "1240x600" || icon.sizes === "2480x1200"
+ )
+ if (desktopScreenshot) {
+ manifest.screenshots?.push({
+ src: desktopScreenshot.src,
+ sizes: desktopScreenshot.sizes,
+ type: "image/png",
+ form_factor: "wide",
+ label: "Desktop view",
+ })
+ }
+
+ const mobileScreenshot = manifest.icons.find(
+ icon => icon.sizes === "620x620" || icon.sizes === "1024x1024"
+ )
+ if (mobileScreenshot) {
+ manifest.screenshots?.push({
+ src: mobileScreenshot.src,
+ sizes: mobileScreenshot.sizes,
+ type: "image/png",
+ label: "Mobile view",
+ })
+ }
} catch (error) {
console.error("Error processing manifest icons:", error)
}
}
- console.log(JSON.stringify(manifest))
+
ctx.set("Content-Type", "application/json")
ctx.body = manifest
} catch (error) {
diff --git a/packages/types/src/documents/app/app.ts b/packages/types/src/documents/app/app.ts
index e2ccc4cbcf..d61ce3c83f 100644
--- a/packages/types/src/documents/app/app.ts
+++ b/packages/types/src/documents/app/app.ts
@@ -101,6 +101,8 @@ export interface PWAManifestImage {
src: string
sizes: string
type: string
+ form_factor?: "wide" | "narrow" | undefined
+ label?: string
}
export interface AppScript {