diff --git a/.github/workflows/build-desktop.yml b/.github/workflows/build-desktop.yml
index 0607e0910..235a6dd3b 100644
--- a/.github/workflows/build-desktop.yml
+++ b/.github/workflows/build-desktop.yml
@@ -32,6 +32,7 @@ env:
VITE_OPENPANEL_CLIENT_ID: ${{ vars.VITE_OPENPANEL_CLIENT_ID }}
VITE_OPENPANEL_API_URL: ${{ vars.VITE_OPENPANEL_API_URL }}
VITE_FIREBASE_CONFIG: ${{ vars.VITE_FIREBASE_CONFIG }}
+ SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
NODE_OPTIONS: --max-old-space-size=8192
jobs:
@@ -137,14 +138,11 @@ jobs:
- name: Build - Vite
working-directory: apps/desktop
run: pnpm build:electron-vite ${{ env.PROD == 'false' && '--mode staging' || '' }}
- env:
- SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
+
- name: Build - Windows and Linux
- if: runner.os != 'macOS'
+ if: runner.os == 'Linux' || (runner.os == 'Windows' && github.event.inputs.store != 'true')
working-directory: apps/desktop
run: pnpm build:electron-forge ${{ env.PROD == 'false' && '--mode=staging' || '' }}
- env:
- SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
- name: Build - macOS
if: runner.os == 'macOS' && github.event.inputs.store != 'true'
@@ -174,12 +172,15 @@ jobs:
working-directory: apps/desktop
run: pnpm build:electron-forge:mas
+ - name: Build - Microsoft Store
+ if: runner.os == 'Windows' && github.event.inputs.store == 'true'
+ working-directory: apps/desktop
+ run: pnpm build:electron-forge:ms
+
- name: Build - Renderer
if: runner.os == 'Linux'
working-directory: apps/desktop
run: pnpm build:render
- env:
- SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
- name: Upload file (macos-arm64-dmg)
uses: actions/upload-artifact@v4
diff --git a/.gitignore b/.gitignore
index e6b9088ef..625406b86 100644
--- a/.gitignore
+++ b/.gitignore
@@ -23,4 +23,6 @@ apps/desktop/src/renderer/dev-dist
tsconfig.tsbuildinfo
buildServer.json
-**/**/generated-routes.ts
\ No newline at end of file
+**/**/generated-routes.ts
+
+apps/desktop/build/appxmanifest.xml
diff --git a/apps/desktop/build/appxmanifest-template.xml b/apps/desktop/build/appxmanifest-template.xml
new file mode 100644
index 000000000..a902bf35a
--- /dev/null
+++ b/apps/desktop/build/appxmanifest-template.xml
@@ -0,0 +1,43 @@
+
+
+
+
+ {packageDisplayName}
+ {publisherDisplayName}
+ No description entered
+ assets\SampleAppx.50x50.png
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {protocol}
+
+
+
+
diff --git a/apps/desktop/forge.config.cts b/apps/desktop/forge.config.cts
index 88b5499e7..dca34077f 100644
--- a/apps/desktop/forge.config.cts
+++ b/apps/desktop/forge.config.cts
@@ -212,6 +212,7 @@ const config: ForgeConfig = {
packageDisplayName: "Folo - Follow everything in one place",
devCert: "build/dev.pfx",
assets: "static/appx",
+ manifest: "build/appxmanifest.xml",
// @ts-ignore
publisherDisplayName: "Natural Selection Labs",
identityName: "NaturalSelectionLabs.Follow-Yourfavoritesinoneinbo",
diff --git a/apps/desktop/package.json b/apps/desktop/package.json
index d5264b1dc..f1f22aa16 100644
--- a/apps/desktop/package.json
+++ b/apps/desktop/package.json
@@ -17,6 +17,7 @@
"build:electron-forge": "electron-forge make",
"build:electron-forge:macos": "electron-forge make --arch=x64 --platform=darwin && electron-forge make --arch=arm64 --platform=darwin && tsx scripts/merge-yml.ts",
"build:electron-forge:mas": "electron-forge make --arch=universal --platform=mas",
+ "build:electron-forge:ms": "tsx scripts/generate-appx-manifest.ts && electron-forge make --platform=win32",
"build:electron-vite": "electron-vite build",
"build:render": "vite build -c vite.config.electron-render.ts",
"build:web": "rm -rf out/web && cross-env WEB_BUILD=1 vite build",
diff --git a/apps/desktop/scripts/generate-appx-manifest.ts b/apps/desktop/scripts/generate-appx-manifest.ts
new file mode 100644
index 000000000..6fcd511d6
--- /dev/null
+++ b/apps/desktop/scripts/generate-appx-manifest.ts
@@ -0,0 +1,110 @@
+#!/usr/bin/env tsx
+
+import fs from "node:fs"
+
+import path from "pathe"
+
+interface AppXManifestConfig {
+ packageName: string
+ packageDisplayName: string
+ publisherDisplayName: string
+ identityName: string
+ version: string
+ publisher: string
+ packageBackgroundColor: string
+ protocols: string[]
+ description: string
+ appBundleId: string
+}
+
+function generateAppXManifest(config: AppXManifestConfig, templatePath: string): string {
+ // Read template file
+ const template = fs.readFileSync(templatePath, "utf-8")
+
+ // Generate protocol extensions
+ const protocolExtensions = config.protocols
+ .map(
+ (protocol) => `
+
+
+ `,
+ )
+ .join("")
+
+ // Replace template variables
+ const manifest = template
+ .replaceAll("{identityName}", config.identityName)
+ .replaceAll("{publisherName}", config.publisher)
+ .replaceAll("{packageVersion}", config.version)
+ .replaceAll("{packageDisplayName}", config.packageDisplayName)
+ .replaceAll("{publisherDisplayName}", config.publisherDisplayName)
+ .replaceAll("{packageName}", config.packageName)
+ .replaceAll("{packageExecutable}", `app\\${config.packageName}.exe`)
+ .replaceAll("{packageBackgroundColor}", config.packageBackgroundColor)
+ .replaceAll("{packageDescription}", config.description)
+ .replaceAll("{protocol}", protocolExtensions)
+
+ return manifest
+}
+
+async function main() {
+ try {
+ // Read package.json to get app information
+ const packageJsonPath = path.resolve(process.cwd(), "package.json")
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"))
+
+ // Get mode from command line arguments
+ const mode = process.argv.find((arg) => arg.startsWith("--mode"))?.split("=")[1]
+ const isStaging = mode === "staging"
+
+ // Parse version for AppX format (must be x.x.x.x)
+ const { version } = packageJson
+ const versionParts = version.split(".")
+ // Ensure we have 4 parts for AppX version format
+ while (versionParts.length < 4) {
+ versionParts.push("0")
+ }
+ const appxVersion = versionParts.slice(0, 4).join(".")
+
+ const config: AppXManifestConfig = {
+ packageName: "Folo",
+ packageDisplayName: isStaging
+ ? "Folo Staging - Follow everything in one place"
+ : "Folo - Follow everything in one place",
+ publisherDisplayName: "Natural Selection Labs",
+ identityName: "NaturalSelectionLabs.Follow-Yourfavoritesinoneinbo",
+ version: appxVersion,
+ publisher: "CN=7CBBEB6A-9B0E-4387-BAE3-576D0ACA279E",
+ packageBackgroundColor: "#FF5C00",
+ protocols: ["folo", "follow"],
+ description: "Follow everything in one place.",
+ appBundleId: "is.follow",
+ }
+
+ // Template file path
+ const templatePath = path.resolve(process.cwd(), "build/appxmanifest-template.xml")
+
+ if (!fs.existsSync(templatePath)) {
+ throw new Error(`Template file not found: ${templatePath}`)
+ }
+
+ const manifest = generateAppXManifest(config, templatePath)
+
+ // Ensure output directory exists
+ const outputDir = path.resolve(process.cwd(), "build")
+ if (!fs.existsSync(outputDir)) {
+ fs.mkdirSync(outputDir, { recursive: true })
+ }
+
+ // Write manifest file
+ const outputPath = path.resolve(outputDir, "appxmanifest.xml")
+ fs.writeFileSync(outputPath, manifest, "utf-8")
+ } catch (error) {
+ console.error("❌ Failed to generate AppX manifest:", error)
+ process.exit(1)
+ }
+}
+
+main()
+
+export { type AppXManifestConfig, generateAppXManifest }