diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index bf836d1afd..3a29eaf0cf 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -9,8 +9,15 @@ on: workflow_dispatch: inputs: tag_version: + type: boolean description: "Tag Version" - required: true + mas: + type: boolean + description: "Build for Mac App Store" + build_version: + type: string + description: "Build Version, only available when mas is true" + # https://docs.github.com/en/enterprise-cloud@latest/actions/writing-workflows/choosing-what-your-workflow-does/control-the-concurrency-of-workflows-and-jobs concurrency: group: ${{ github.workflow }}-${{ github.ref }}-build @@ -32,6 +39,9 @@ jobs: fail-fast: false matrix: os: [macos-latest, ubuntu-latest, windows-latest] + exclude: + - os: ${{ github.event.inputs.mas == 'true' && 'ubuntu-latest' }} + - os: ${{ github.event.inputs.mas == 'true' && 'windows-latest' }} permissions: id-token: write @@ -41,13 +51,13 @@ jobs: steps: - name: Check out Git repository Fully uses: actions/checkout@v4 - if: github.event.inputs.tag_version != '' || github.ref_type == 'tag' + if: github.event.inputs.tag_version == 'true' || github.ref_type == 'tag' with: fetch-depth: 0 lfs: true - name: Check out Git repository uses: actions/checkout@v4 - if: github.event.inputs.tag_version == '' && github.ref_type != 'tag' + if: github.event.inputs.tag_version != 'true' && github.ref_type != 'tag' with: fetch-depth: 1 lfs: true @@ -73,17 +83,23 @@ jobs: if: runner.os == 'macOS' env: BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }} + BUILD_CERTIFICATE_MAS_BASE64: ${{ secrets.BUILD_CERTIFICATE_MAS_BASE64 }} + BUILD_CERTIFICATE_MASPKG_BASE64: ${{ secrets.BUILD_CERTIFICATE_MASPKG_BASE64 }} P12_PASSWORD: ${{ secrets.P12_PASSWORD }} BUILD_PROVISION_PROFILE_BASE64: ${{ secrets.BUILD_PROVISION_PROFILE_BASE64 }} KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }} run: | # create variables CERTIFICATE_PATH=$RUNNER_TEMP/build_certificate.p12 + CERTIFICATE_MAS_PATH=$RUNNER_TEMP/build_certificate_mas.p12 + CERTIFICATE_MASPKG_PATH=$RUNNER_TEMP/build_certificate_maspkg.p12 PP_PATH=$RUNNER_TEMP/build_pp.provisionprofile KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db # import certificate and provisioning profile from secrets echo -n "$BUILD_CERTIFICATE_BASE64" | base64 --decode -o $CERTIFICATE_PATH + echo -n "$BUILD_CERTIFICATE_MAS_BASE64" | base64 --decode -o $CERTIFICATE_MAS_PATH + echo -n "$BUILD_CERTIFICATE_MASPKG_BASE64" | base64 --decode -o $CERTIFICATE_MASPKG_PATH echo -n "$BUILD_PROVISION_PROFILE_BASE64" | base64 --decode -o $PP_PATH # create temporary keychain @@ -93,8 +109,11 @@ jobs: # import certificate to keychain security import $CERTIFICATE_PATH -P "$P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH + security import $CERTIFICATE_MAS_PATH -P "$P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH + security import $CERTIFICATE_MASPKG_PATH -P "$P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH security set-key-partition-list -S apple-tool:,apple: -k "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH security list-keychain -d user -s $KEYCHAIN_PATH + security find-identity $KEYCHAIN_PATH # apply provisioning profile mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles @@ -111,14 +130,24 @@ jobs: SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} - name: Build (macOS) - if: matrix.os == 'macos-latest' + if: matrix.os == 'macos-latest' && github.event.inputs.mas != 'true' env: APPLE_ID: ${{ secrets.APPLE_ID }} APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }} APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} - KEYCHAIN_PATH: ${{ runner.temp }}/app-signing.keychain-db + OSX_SIGN_KEYCHAIN_PATH: ${{ runner.temp }}/app-signing.keychain-db + OSX_SIGN_IDENTITY: ${{ secrets.OSX_SIGN_IDENTITY }} run: npm exec turbo run //#build:macos + - name: Build (Mac App Store) + if: matrix.os == 'macos-latest' && github.event.inputs.mas == 'true' + env: + OSX_SIGN_KEYCHAIN_PATH: ${{ runner.temp }}/app-signing.keychain-db + OSX_SIGN_IDENTITY: ${{ secrets.OSX_SIGN_IDENTITY_MAS }} + OSX_SIGN_PROVISIONING_PROFILE_PATH: ${{ runner.temp }}/build_pp.provisionprofile + BUILD_VERSION: ${{ github.event.inputs.build_version }} + run: npm exec turbo run //#build:mas + - name: Build Renderer if: matrix.os == 'ubuntu-latest' run: pnpm build:render @@ -138,6 +167,7 @@ jobs: out/make/**/*.exe out/make/**/*.AppImage out/make/**/*.yml + out/make/**/*.pkg dist/manifest.yml dist/*.tar.gz retention-days: 90 @@ -153,8 +183,19 @@ jobs: out/make/**/*arm64.dmg retention-days: 90 + - name: Upload file (pkg) + uses: actions/upload-artifact@v4 + if: matrix.os == 'macos-latest' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + name: mas-pkg + path: | + out/make/**/*.pkg + retention-days: 90 + - name: Generate artifact attestation - if: github.ref_type == 'tag' || github.event.inputs.tag_version != '' + if: github.ref_type == 'tag' || github.event.inputs.tag_version == 'true' continue-on-error: true uses: actions/attest-build-provenance@v1 with: @@ -168,7 +209,7 @@ jobs: dist/*.tar.gz - run: npx changelogithub - if: github.ref_type == 'tag' || github.event.inputs.tag_version != '' + if: github.ref_type == 'tag' || github.event.inputs.tag_version == 'true' continue-on-error: true env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -178,7 +219,7 @@ jobs: uses: ./.github/actions/setup-version - name: Create Release Draft - if: github.ref_type == 'tag' || github.event.inputs.tag_version != '' + if: github.ref_type == 'tag' || github.event.inputs.tag_version == 'true' uses: softprops/action-gh-release@v2 with: name: v${{ steps.version.outputs.APP_VERSION }} diff --git a/apps/renderer/src/modules/auth/LoginModalContent.tsx b/apps/renderer/src/modules/auth/LoginModalContent.tsx index 1289916c44..81b714860f 100644 --- a/apps/renderer/src/modules/auth/LoginModalContent.tsx +++ b/apps/renderer/src/modules/auth/LoginModalContent.tsx @@ -45,43 +45,47 @@ export const LoginModalContent = (props: LoginModalContentProps) => { -
-
- -

{t("login.or")}

- -
-
-
- {Object.entries(authProviders || []) - .filter(([key]) => key !== "credential") - .map(([key, provider]) => ( - - - { - loginHandler(key, "app") - }} - > -
- - - - - {t("login.continueWith", { provider: provider.name })} - - - - ))} -
+ {!process.mas && ( + <> +
+
+ +

{t("login.or")}

+ +
+
+
+ {Object.entries(authProviders || []) + .filter(([key]) => key !== "credential") + .map(([key, provider]) => ( + + + { + loginHandler(key, "app") + }} + > +
+ + + + + {t("login.continueWith", { provider: provider.name })} + + + + ))} +
+ + )} ) if (isMobile) { diff --git a/apps/renderer/src/pages/settings/(settings)/profile.tsx b/apps/renderer/src/pages/settings/(settings)/profile.tsx index 496c695273..8352b7f98c 100644 --- a/apps/renderer/src/pages/settings/(settings)/profile.tsx +++ b/apps/renderer/src/pages/settings/(settings)/profile.tsx @@ -1,5 +1,9 @@ +import { Button } from "@follow/components/ui/button/index.js" import { Divider } from "@follow/components/ui/divider/Divider.js" +import { Label } from "@follow/components/ui/label/index.js" +import { signOut } from "@follow/shared/auth" +import { useModalStack } from "~/components/ui/modal/stacked/hooks" import { AccountManagement } from "~/modules/profile/account-management" import { EmailManagement } from "~/modules/profile/email-management" import { ProfileSettingForm } from "~/modules/profile/profile-setting-form" @@ -17,6 +21,7 @@ export const loader = defineSettingPageData({ }) export function Component() { + const { present } = useModalStack() return ( <> @@ -30,6 +35,39 @@ export function Component() { + {/* TODO: Temporary fake account deletion feature */} + {process.mas && ( +
+ + +
+ ), + }) + }} + > + Delete + +
+ )}
diff --git a/build/entitlements.mas.child.plist b/build/entitlements.mas.child.plist new file mode 100644 index 0000000000..d8dc69e808 --- /dev/null +++ b/build/entitlements.mas.child.plist @@ -0,0 +1,10 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.inherit + + + diff --git a/build/entitlements.mas.plist b/build/entitlements.mas.plist new file mode 100644 index 0000000000..81caf94414 --- /dev/null +++ b/build/entitlements.mas.plist @@ -0,0 +1,16 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.files.user-selected.read-write + + com.apple.security.files.bookmarks.app-scope + + com.apple.security.network.client + + ITSAppUsesNonExemptEncryption + + + diff --git a/forge.config.cts b/forge.config.cts index a1a57906dc..13da196e46 100644 --- a/forge.config.cts +++ b/forge.config.cts @@ -1,3 +1,5 @@ +import "dotenv/config" + import crypto from "node:crypto" import fs, { readdirSync } from "node:fs" import { cp, readdir } from "node:fs/promises" @@ -5,6 +7,7 @@ import path, { resolve } from "node:path" import { FuseV1Options, FuseVersion } from "@electron/fuses" import { MakerDMG } from "@electron-forge/maker-dmg" +import { MakerPKG } from "@electron-forge/maker-pkg" import { MakerSquirrel } from "@electron-forge/maker-squirrel" import { MakerZIP } from "@electron-forge/maker-zip" import { FusesPlugin } from "@electron-forge/plugin-fuses" @@ -14,6 +17,8 @@ import setLanguages from "electron-packager-languages" import yaml from "js-yaml" import { rimraf, rimrafSync } from "rimraf" +const platform = process.argv[process.argv.indexOf("--platform") + 1] + const artifactRegex = /.*\.(?:exe|dmg|AppImage|zip)$/ const platformNamesMap = { darwin: "macos", @@ -34,7 +39,7 @@ async function cleanSources(buildPath, _electronVersion, platform, _arch, callba // folders & files to be included in the app const appItems = new Set(["dist", "node_modules", "package.json", "resources"]) - if (platform === "darwin") { + if (platform === "darwin" || platform === "mas") { const frameworkResourcePath = resolve( buildPath, "../../Frameworks/Electron Framework.framework/Versions/A/Resources", @@ -87,6 +92,8 @@ const ignorePattern = new RegExp(`^/node_modules/(?!${[...keepModules].join("|") const config: ForgeConfig = { packagerConfig: { + appCategoryType: "public.app-category.news", + buildVersion: process.env.BUILD_VERSION || undefined, appBundleId: "is.follow", icon: "resources/icon", extraResource: ["./resources/app-update.yml"], @@ -105,15 +112,28 @@ const config: ForgeConfig = { ignore: [ignorePattern], prune: true, + osxSign: { + optionsForFile: + platform === "mas" + ? (filePath) => { + const entitlements = filePath.includes(".app/") + ? "build/entitlements.mas.child.plist" + : "build/entitlements.mas.plist" + return { + hardenedRuntime: false, + entitlements, + } + } + : () => ({ + entitlements: "build/entitlements.mac.plist", + }), + keychain: process.env.OSX_SIGN_KEYCHAIN_PATH, + identity: process.env.OSX_SIGN_IDENTITY, + provisioningProfile: process.env.OSX_SIGN_PROVISIONING_PROFILE_PATH, + }, ...(process.env.APPLE_ID && process.env.APPLE_PASSWORD && process.env.APPLE_TEAM_ID && { - osxSign: { - optionsForFile: () => ({ - entitlements: "build/entitlements.mac.plist", - }), - keychain: process.env.KEYCHAIN_PATH, - }, osxNotarize: { appleId: process.env.APPLE_ID!, appleIdPassword: process.env.APPLE_PASSWORD!, @@ -124,34 +144,37 @@ const config: ForgeConfig = { rebuildConfig: {}, makers: [ new MakerZIP({}, ["darwin"]), - new MakerDMG({ - overwrite: true, - background: "static/dmg-background.png", - icon: "static/dmg-icon.icns", - iconSize: 160, - additionalDMGOptions: { - window: { - size: { - width: 660, - height: 400, + new MakerDMG( + { + overwrite: true, + background: "static/dmg-background.png", + icon: "static/dmg-icon.icns", + iconSize: 160, + additionalDMGOptions: { + window: { + size: { + width: 660, + height: 400, + }, }, }, + contents: (opts) => [ + { + x: 180, + y: 170, + type: "file", + path: (opts as any).appPath, + }, + { + x: 480, + y: 170, + type: "link", + path: "/Applications", + }, + ], }, - contents: (opts) => [ - { - x: 180, - y: 170, - type: "file", - path: (opts as any).appPath, - }, - { - x: 480, - y: 170, - type: "link", - path: "/Applications", - }, - ], - }), + ["darwin", "mas"], + ), new MakerSquirrel({ name: "Follow", setupIcon: "resources/icon.ico", @@ -167,6 +190,13 @@ const config: ForgeConfig = { ], }, }), + new MakerPKG( + { + name: "Follow", + keychain: process.env.KEYCHAIN_PATH, + }, + ["mas"], + ), ], plugins: [ // Fuses are used to enable/disable various Electron functionality diff --git a/package.json b/package.json index e192829729..021d6ff79a 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "analyze:web": "analyzer=1 vite build", "build": "electron-vite build && electron-forge make", "build:macos": "electron-vite build && electron-forge make --arch=x64 --platform=darwin && electron-forge make --arch=arm64 --platform=darwin && tsx scripts/merge-yml.ts", + "build:mas": "electron-vite build && electron-forge make --arch=universal --platform=mas", "build:render": "vite build -c vite.config.electron-render.ts", "build:rn-web": "rm -rf out/rn-web && cross-env RN_BUILD=1 vite build", "build:web": "rm -rf out/web && cross-env WEB_BUILD=1 vite build", @@ -52,6 +53,7 @@ "@egoist/tailwindcss-icons": "1.9.0", "@electron-forge/cli": "7.6.1", "@electron-forge/maker-dmg": "7.6.1", + "@electron-forge/maker-pkg": "7.6.1", "@electron-forge/maker-squirrel": "7.6.1", "@electron-forge/maker-zip": "7.6.1", "@electron-forge/plugin-fuses": "7.6.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2114eae409..f2b9a99641 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -73,6 +73,9 @@ importers: '@electron-forge/maker-dmg': specifier: 7.6.1 version: 7.6.1 + '@electron-forge/maker-pkg': + specifier: 7.6.1 + version: 7.6.1 '@electron-forge/maker-squirrel': specifier: 7.6.1 version: 7.6.1 @@ -2433,6 +2436,10 @@ packages: resolution: {integrity: sha512-D7cJRE6CGeovLZhu2dRqUm3w/AlkTURYJYgFuUsgwpBuviQKgJd8quZar6IeZ/l83y4Z1dghKb8D3aAj+bRvNQ==} engines: {node: '>= 16.4.0'} + '@electron-forge/maker-pkg@7.6.1': + resolution: {integrity: sha512-AmbfMqwgi40w7AW5AuGJhDedM4XOiF1hb3aH1OX5KM4ZzNtlRB5ThKArALxt6YxosBfjS5FF2RBXNjb7qtuiCQ==} + engines: {node: '>= 16.4.0'} + '@electron-forge/maker-squirrel@7.6.1': resolution: {integrity: sha512-7EMLcl0QM5GfdY+enfauEqV6ZW14A1S6Eqoev812FXGTm88G8Ik0tPRw6SsIaI8R++YqxsbdCGTQjzdJWY0bJA==} engines: {node: '>= 16.4.0'} @@ -16258,6 +16265,15 @@ snapshots: - bluebird - supports-color + '@electron-forge/maker-pkg@7.6.1': + dependencies: + '@electron-forge/maker-base': 7.6.1 + '@electron-forge/shared-types': 7.6.1 + '@electron/osx-sign': 1.3.2 + transitivePeerDependencies: + - bluebird + - supports-color + '@electron-forge/maker-squirrel@7.6.1': dependencies: '@electron-forge/maker-base': 7.6.1 diff --git a/turbo.json b/turbo.json index 52fd871896..1a4296c0fe 100644 --- a/turbo.json +++ b/turbo.json @@ -11,6 +11,9 @@ "//#build:macos": { "outputs": ["dist/**", "out/**"] }, + "//#build:mas": { + "outputs": ["dist/**", "out/**"] + }, "//#format:check": {}, "//#lint": {}, "test": {},