diff --git a/app/package.json b/app/package.json index 2f1d71ea..74ffbb52 100644 --- a/app/package.json +++ b/app/package.json @@ -5,6 +5,8 @@ "scripts": { "dev": "webpack serve --config webpack.dev.js", "build": "webpack --config webpack.prod.js", + "dev:electron": "webpack serve --config webpack.electron.dev.js", + "build:electron": "webpack --config webpack.electron.prod.js", "serve": "npx http-server dist", "test": "jest", "format": "prettier --write src", diff --git a/app/src/components/App/ElectronCallbackHandler.tsx b/app/src/components/App/ElectronCallbackHandler.tsx index c784810f..86d8ffda 100644 --- a/app/src/components/App/ElectronCallbackHandler.tsx +++ b/app/src/components/App/ElectronCallbackHandler.tsx @@ -175,7 +175,7 @@ export const ElectronCallbackHandler: FC = observer(() => { try { await signInWithCredential(auth, credential) } catch (e) { - toast.error("Failed to sign in with Google") + toast.error("Failed to sign in") } }, ), diff --git a/app/webpack.common.js b/app/webpack.common.js index ec6c9bff..1e3109e6 100644 --- a/app/webpack.common.js +++ b/app/webpack.common.js @@ -3,7 +3,7 @@ const HtmlWebpackPlugin = require("html-webpack-plugin") const webpack = require("webpack") const Dotenv = require("dotenv-webpack") -module.exports = (env) => ({ +module.exports = { context: __dirname, entry: { browserMain: "./src/index.tsx", @@ -49,4 +49,4 @@ module.exports = (env) => ({ template: path.join(__dirname, "public", "community.html"), }), ], -}) +} diff --git a/app/webpack.dev.js b/app/webpack.dev.js index fe8da223..b4db3e22 100644 --- a/app/webpack.dev.js +++ b/app/webpack.dev.js @@ -4,7 +4,7 @@ const path = require("path") const ForkTsCheckerWebpackPlugin = require("fork-ts-checker-webpack-plugin") const ReactRefreshWebpackPlugin = require("@pmmmwh/react-refresh-webpack-plugin") -const config = { +module.exports = merge(common, { mode: "development", devtool: "inline-source-map", devServer: { @@ -64,6 +64,4 @@ const config = { react: path.resolve("../node_modules/react"), }, }, -} - -module.exports = (env) => merge(common(env), config) +}) diff --git a/app/webpack.electron.common.js b/app/webpack.electron.common.js new file mode 100644 index 00000000..d620bac8 --- /dev/null +++ b/app/webpack.electron.common.js @@ -0,0 +1,38 @@ +const path = require("path") +const HtmlWebpackPlugin = require("html-webpack-plugin") +const webpack = require("webpack") +const Dotenv = require("dotenv-webpack") + +module.exports = { + context: __dirname, + entry: { + browserMain: "./src/index.tsx", + }, + output: { + filename: "[name]-[chunkhash].js", + clean: true, + }, + module: { + rules: [ + { + test: /\.(png|jpg|jpeg|gif|woff|woff2|eot|ttf)$/, + loader: "url-loader", + }, + ], + }, + resolve: { + extensions: [".js", ".jsx", ".ts", ".tsx"], + }, + plugins: [ + new Dotenv({ + path: path.join(__dirname, "../.env"), + systemvars: true, + }), + new HtmlWebpackPlugin({ + inject: true, + filename: "edit.html", + chunks: ["browserMain"], + template: path.join(__dirname, "public", "edit.html"), + }), + ], +} diff --git a/app/webpack.electron.dev.js b/app/webpack.electron.dev.js new file mode 100644 index 00000000..57506b8f --- /dev/null +++ b/app/webpack.electron.dev.js @@ -0,0 +1,58 @@ +const { merge } = require("webpack-merge") +const common = require("./webpack.common.js") +const path = require("path") +const ForkTsCheckerWebpackPlugin = require("fork-ts-checker-webpack-plugin") +const ReactRefreshWebpackPlugin = require("@pmmmwh/react-refresh-webpack-plugin") + +module.exports = merge(common, { + mode: "development", + devtool: "inline-source-map", + devServer: { + port: 3000, + hot: "only", + static: { + directory: path.resolve(__dirname, "public"), + watch: true, + }, + client: { + overlay: { + warnings: false, + errors: true, + }, + }, + historyApiFallback: { + rewrites: [{ from: /^\/edit$/, to: "/edit.html" }], + }, + }, + module: { + rules: [ + { + test: /\.(j|t)sx?$/, + exclude: /node_modules/, + use: { + loader: "babel-loader", + options: { + plugins: [require.resolve("react-refresh/babel")], + }, + }, + }, + { + test: /\.js$/, + enforce: "pre", + use: ["source-map-loader"], + }, + ], + }, + plugins: [ + new ForkTsCheckerWebpackPlugin(), + new ReactRefreshWebpackPlugin({ + exclude: [/node_modules/], + }), + ], + resolve: { + alias: { + // Prevent to load local package's react https://github.com/facebook/react/issues/13991#issuecomment-435587809 + react: path.resolve("../node_modules/react"), + }, + }, +}) diff --git a/app/webpack.electron.prod.js b/app/webpack.electron.prod.js new file mode 100644 index 00000000..e6530c78 --- /dev/null +++ b/app/webpack.electron.prod.js @@ -0,0 +1,35 @@ +const { merge } = require("webpack-merge") +const common = require("./webpack.common.js") +const CopyPlugin = require("copy-webpack-plugin") +const { sentryWebpackPlugin } = require("@sentry/webpack-plugin") +const WorkboxPlugin = require("workbox-webpack-plugin") + +module.exports = merge(common, { + mode: "production", + optimization: { + concatenateModules: false, + splitChunks: { + chunks: "all", + }, + }, + module: { + rules: [ + { + test: /\.(j|t)sx?$/, + exclude: /node_modules/, + use: { + loader: "babel-loader", + }, + }, + ], + }, + plugins: [ + new CopyPlugin({ + patterns: [ + { from: "public/*.svg", to: "[name][ext]" }, + { from: "public/*.png", to: "[name][ext]" }, + { from: "public/*.webmanifest", to: "[name][ext]" }, + ], + }), + ], +}) diff --git a/app/webpack.prod.js b/app/webpack.prod.js index 3336b053..26313ce5 100644 --- a/app/webpack.prod.js +++ b/app/webpack.prod.js @@ -4,7 +4,7 @@ const CopyPlugin = require("copy-webpack-plugin") const { sentryWebpackPlugin } = require("@sentry/webpack-plugin") const WorkboxPlugin = require("workbox-webpack-plugin") -const config = (env) => ({ +module.exports = merge(common, { mode: "production", optimization: { concatenateModules: false, @@ -24,33 +24,29 @@ const config = (env) => ({ ], }, plugins: [ - ...(env.electron - ? [] - : [ - new WorkboxPlugin.GenerateSW({ - maximumFileSizeToCacheInBytes: 50000000, - clientsClaim: true, - skipWaiting: true, - runtimeCaching: [ - { - urlPattern: /^\/.*$/, - handler: "StaleWhileRevalidate", - }, - { - urlPattern: /^.+\.sf2$/, - handler: "StaleWhileRevalidate", - }, - { - urlPattern: /^https:\/\/fonts\.googleapis\.com/, - handler: "StaleWhileRevalidate", - }, - { - urlPattern: /^https:\/\/fonts\.gstatic\.com/, - handler: "StaleWhileRevalidate", - }, - ], - }), - ]), + new WorkboxPlugin.GenerateSW({ + maximumFileSizeToCacheInBytes: 50000000, + clientsClaim: true, + skipWaiting: true, + runtimeCaching: [ + { + urlPattern: /^\/.*$/, + handler: "StaleWhileRevalidate", + }, + { + urlPattern: /^.+\.sf2$/, + handler: "StaleWhileRevalidate", + }, + { + urlPattern: /^https:\/\/fonts\.googleapis\.com/, + handler: "StaleWhileRevalidate", + }, + { + urlPattern: /^https:\/\/fonts\.gstatic\.com/, + handler: "StaleWhileRevalidate", + }, + ], + }), new CopyPlugin({ patterns: [ { from: "public/*.svg", to: "[name][ext]" }, @@ -78,5 +74,3 @@ const config = (env) => ({ }), ], }) - -module.exports = (env) => merge(common(env), config(env)) diff --git a/electron/package-lock.json b/electron/package-lock.json index 36d3d4be..ca57be90 100644 --- a/electron/package-lock.json +++ b/electron/package-lock.json @@ -10,7 +10,8 @@ "dependencies": { "dotenv": "^16.4.5", "electron-log": "^5.1.2", - "electron-window-state": "^5.0.3" + "electron-window-state": "^5.0.3", + "node-mac-sign-in-with-apple": "github:thomasdao/node-mac-sign-in-with-apple" }, "devDependencies": { "@electron-forge/cli": "7.3.0", @@ -2311,6 +2312,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, "node_modules/bl": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", @@ -3292,6 +3301,11 @@ "pend": "~1.2.0" } }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==" + }, "node_modules/filename-reserved-regex": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/filename-reserved-regex/-/filename-reserved-regex-2.0.0.tgz", @@ -4670,6 +4684,14 @@ "node": ">=10" } }, + "node_modules/node-addon-api": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.0.0.tgz", + "integrity": "sha512-ipO7rsHEBqa9STO5C5T10fj732ml+5kLN1cAG8/jdHd56ldQeGj3Q7+scUS+VHK/qy1zLEwC4wMK5+yM0btPvw==", + "engines": { + "node": "^18 || ^20 || >= 21" + } + }, "node_modules/node-api-version": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/node-api-version/-/node-api-version-0.2.0.tgz", @@ -4754,6 +4776,19 @@ "node": ">=10" } }, + "node_modules/node-mac-sign-in-with-apple": { + "version": "0.5.0", + "resolved": "git+ssh://git@github.com/thomasdao/node-mac-sign-in-with-apple.git#6840b9a36cedd65c6502f1dda65304c9ffea001d", + "hasInstallScript": true, + "license": "MIT", + "os": [ + "darwin" + ], + "dependencies": { + "bindings": "^1.5.0", + "node-addon-api": "^8.0.0" + } + }, "node_modules/nodemon": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.0.tgz", diff --git a/electron/package.json b/electron/package.json index d16cecea..1b4bdf4f 100644 --- a/electron/package.json +++ b/electron/package.json @@ -8,7 +8,6 @@ "dev": "concurrently \"npm run build -- --watch\" \"nodemon\"", "start": "npm run build && electron .", "build": "rollup -c --bundleConfigAsCjs", - "build:swift": "./scripts/build_swift.sh", "package:darwin": "npm run build && electron-forge package --platform=darwin", "make": "npm run build && electron-forge make", "make:mas": "npm run make -- --arch=universal --platform=mas", @@ -33,6 +32,7 @@ "dependencies": { "dotenv": "^16.4.5", "electron-log": "^5.1.2", - "electron-window-state": "^5.0.3" + "electron-window-state": "^5.0.3", + "node-mac-sign-in-with-apple": "github:thomasdao/node-mac-sign-in-with-apple" } } diff --git a/electron/resources/AuthSession_mac b/electron/resources/AuthSession_mac deleted file mode 100755 index 07c51ff7..00000000 Binary files a/electron/resources/AuthSession_mac and /dev/null differ diff --git a/electron/rollup.config.js b/electron/rollup.config.js index 094f0816..47fd8305 100644 --- a/electron/rollup.config.js +++ b/electron/rollup.config.js @@ -11,13 +11,7 @@ export default [ format: "cjs", }, external: ["electron"], - plugins: [ - nodeResolve({ preferBuiltins: true }), - commonjs(), - typescript({ - tsconfig: "tsconfig.preload.json", - }), - ], + plugins: [nodeResolve({ preferBuiltins: true }), commonjs(), typescript()], }, { input: "src/index.ts", diff --git a/electron/scripts/build_swift.sh b/electron/scripts/build_swift.sh deleted file mode 100755 index 58509f19..00000000 --- a/electron/scripts/build_swift.sh +++ /dev/null @@ -1,13 +0,0 @@ -# Generate the binary for x86_64 architecture -swiftc -target x86_64-apple-macos10.15 -o AuthSession-x86_64 ./src/AuthSession.swift - -# Generate the binary for arm64 architecture -swiftc -target arm64-apple-macos11 -o AuthSession-arm64 ./src/AuthSession.swift - -# Use the lipo command to create a universal binary -lipo -create -output ./resources/AuthSession_mac AuthSession-x86_64 AuthSession-arm64 - -# Verify the universal binary -file ./resources/AuthSession_mac - -rm AuthSession-x86_64 AuthSession-arm64 diff --git a/electron/src/AuthSession.swift b/electron/src/AuthSession.swift deleted file mode 100644 index 839e3ff1..00000000 --- a/electron/src/AuthSession.swift +++ /dev/null @@ -1,29 +0,0 @@ -import Foundation -import AuthenticationServices - -let url = URL(string: CommandLine.arguments[1])! -let callbackURLScheme = CommandLine.arguments[2] - -class AuthSessionDelegate: NSObject, ASWebAuthenticationPresentationContextProviding { - func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor { - return ASPresentationAnchor() - } -} - -let delegate = AuthSessionDelegate() - -let session = ASWebAuthenticationSession(url: url, callbackURLScheme: callbackURLScheme) { callbackURL, error in - if let error = error { - print("Error: \(error.localizedDescription)") - exit(1) - } - if let callbackURL = callbackURL { - print(callbackURL.absoluteString) - exit(0) - } -} - -session.presentationContextProvider = delegate -session.start() - -RunLoop.current.run() diff --git a/electron/src/auth.ts b/electron/src/auth.ts index f4040038..d820941d 100644 --- a/electron/src/auth.ts +++ b/electron/src/auth.ts @@ -1,8 +1,7 @@ -import { execFile } from "child_process" -import { app, shell } from "electron" +import { BrowserWindow, app } from "electron" import log from "electron-log" -import path from "path" -import { appScheme, authCallbackUrl } from "./scheme" +import { FirebaseCredential } from "./ipc" +import { authCallbackUrl } from "./scheme" const authURL = (redirectUri: string) => { const parameter = `redirect_uri=${redirectUri}` @@ -12,39 +11,40 @@ const authURL = (redirectUri: string) => { : `http://localhost:3000/auth?${parameter}` } -// In the mas build, return callbackURL when authentication is completed, and do not return anything when the application is launched with a schema. -export const signInWithBrowser = async (): Promise => { - if (process.mas) { - const callbackURL = await startAuthSession( - authURL(authCallbackUrl), - appScheme, - ) - return callbackURL - } - const url = authURL(authCallbackUrl) - shell.openExternal(url) - return null -} - -const startAuthSession = async ( - url: string, - callbackURLScheme: string, -): Promise => { +export const signInWithBrowser = async (): Promise => { return new Promise((resolve, reject) => { - log.info("electron:auth:startAuthSession", url, callbackURLScheme) - execFile( - path.join(__dirname, "..", "resources", "AuthSession_mac"), - [url, callbackURLScheme], - (error, stdout, stderr) => { - if (error) { - log.error("electron:auth:startAuthSession", error, stderr) - reject(error) - } else { - log.info("electron:auth:startAuthSession", stdout) - const callbackURL = stdout.trim() - resolve(callbackURL) - } + const url = authURL(authCallbackUrl) + const window = new BrowserWindow({ + width: 800, + height: 600, + webPreferences: { + nodeIntegration: false, + contextIsolation: true, + sandbox: false, }, - ) + }) + window.loadURL(url) + + window.webContents.on("will-navigate", (event, url) => { + log.info("will-navigate", url) + if (url.startsWith(authCallbackUrl)) { + log.info("authCallbackUrl", url) + window.close() + + // get ID token from the URL + const urlObj = new URL(url) + const credential = urlObj.searchParams.get("credential") + + if (credential === null) { + log.error("electron:event:open-url", "ID Token is missing") + reject(new Error("ID Token is missing")) + return + } + + log.info("electron:event:open-url", "ID Token is received") + + resolve(JSON.parse(credential)) + } + }) }) } diff --git a/electron/src/index.ts b/electron/src/index.ts index 30367c1d..136bd8ef 100644 --- a/electron/src/index.ts +++ b/electron/src/index.ts @@ -7,7 +7,6 @@ import { defaultMenuTemplate } from "./defaultMenu" import { Ipc } from "./ipc" import { registerIpcMain } from "./ipcMain" import { menuTemplate } from "./menu" -import { authCallbackUrl } from "./scheme" const isMas = process.mas === true @@ -76,8 +75,10 @@ registerIpcMain({ onMainWindowClose() { mainWindow.destroy() }, - onAuthCallback(url) { - handleAuthCallback(url) + onAuthCallback(credential) { + log.info("electron:event:open-url", "ID Token is received") + mainWindow.focus() + ipc.send("onBrowserSignInCompleted", { credential }) }, }) @@ -203,28 +204,6 @@ app.on("browser-window-focus", (_event, window) => { Menu.setApplicationMenu(window === mainWindow ? mainMenu : defaultMenu) }) -app.on("open-url", (_event, url) => { - log.info("electron:event:open-url", url) - if (url.startsWith(authCallbackUrl)) { - handleAuthCallback(url) - } -}) - -function handleAuthCallback(url: string) { - // get ID token from the URL - const urlObj = new URL(url) - const credential = urlObj.searchParams.get("credential") - if (credential === null) { - log.error("electron:event:open-url", "ID Token is missing") - } else { - log.info("electron:event:open-url", "ID Token is received") - mainWindow.focus() - ipc.send("onBrowserSignInCompleted", { - credential: JSON.parse(credential), - }) - } -} - function openSupportPage() { shell.openExternal("https://signal.vercel.app/support") } diff --git a/electron/src/ipcMain.ts b/electron/src/ipcMain.ts index cf6ca362..d9485b0c 100644 --- a/electron/src/ipcMain.ts +++ b/electron/src/ipcMain.ts @@ -1,14 +1,16 @@ import { IpcMainInvokeEvent, app, dialog, ipcMain } from "electron" +import log from "electron-log" import { readFile, readdir, writeFile } from "fs/promises" import { isAbsolute, join } from "path" import { getArgument } from "./arguments" import { signInWithBrowser } from "./auth" +import { FirebaseCredential } from "./ipc" interface Callbacks { onReady: () => void onAuthStateChanged: (isLoggedIn: boolean) => void onMainWindowClose: () => void - onAuthCallback: (url: string) => void + onAuthCallback: (credential: FirebaseCredential) => void } const api = ({ @@ -74,9 +76,11 @@ const api = ({ }, getArgument: async () => getArgument(), openAuthWindow: async () => { - const callbackURL = await signInWithBrowser() - if (callbackURL) { - onAuthCallback(callbackURL) + try { + const credential = await signInWithBrowser() + onAuthCallback(credential) + } catch (e) { + log.error(e) } }, authStateChanged: (_e: IpcMainInvokeEvent, isLoggedIn: boolean) => { diff --git a/electron/tsconfig.json b/electron/tsconfig.json index 11b5afed..120c4bb7 100644 --- a/electron/tsconfig.json +++ b/electron/tsconfig.json @@ -4,7 +4,7 @@ "module": "esnext", "allowJs": false, "noImplicitAny": true, - "baseUrl": ".", + "baseUrl": "./src", "outDir": "dist", "moduleResolution": "node", "resolveJsonModule": true, @@ -15,6 +15,5 @@ "strict": true, "forceConsistentCasingInFileNames": true }, - "include": ["src/*.ts"], - "exclude": ["src/preload.ts"] + "include": ["src/**/*"] } diff --git a/electron/tsconfig.preload.json b/electron/tsconfig.preload.json deleted file mode 100644 index b1c091e4..00000000 --- a/electron/tsconfig.preload.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "compilerOptions": { - "target": "esnext", - "module": "esnext", - "allowJs": false, - "noImplicitAny": true, - "baseUrl": ".", - "outDir": "dist_preload", - "moduleResolution": "node", - "esModuleInterop": true, - "lib": ["ES2015"], - "skipLibCheck": true, - "allowSyntheticDefaultImports": true, - "strict": true, - "forceConsistentCasingInFileNames": true - }, - "include": ["src/preload.ts"] -} diff --git a/package.json b/package.json index 264e5c49..9884f7da 100644 --- a/package.json +++ b/package.json @@ -13,8 +13,8 @@ "format": "turbo format", "firebase": "npm --prefix functions run build && firebase emulators:start", "firebase:deploy": "firebase deploy", - "dev:electron": "concurrently \"npm start -- --env electron --no-open\" \"npm run dev --prefix electron\"", - "build:electron": "npm run build -w app -- --env electron && rm -rf electron/dist_renderer && cp -r dist electron/dist_renderer", + "dev:electron": "concurrently \"npm run dev:electron -w app\" \"npm run dev --prefix electron\"", + "build:electron": "npm run build:electron -w app && rm -rf electron/dist_renderer && cp -r app/dist electron/dist_renderer", "package:electron": "npm run build:electron && npm run package:darwin --prefix electron", "make:electron": "npm run build:electron && npm run make:mas --prefix electron", "make:darwin": "npm run build:electron && npm run make:darwin --prefix electron"