fix OS issues

This commit is contained in:
2025-07-19 04:01:35 +03:00
parent 0b90e5466f
commit 61eb39456d
8 changed files with 265 additions and 253 deletions

View File

@@ -56,16 +56,14 @@ const checkForUpdates = async () => {
// Initialize the app
await myCapacitorApp.init();
// 🔧 Inject additional arguments for preload here:
const userDataPath = app.getPath('userData');
const win = myCapacitorApp.getMainWindow();
if (win) {
win.webContents.session.setPreloads([path.join(__dirname, 'preload.js')]); // optional if not using capacitor-managed preload
win.webContents.setWindowOpenHandler(() => ({ action: 'deny' }));
// ⚠ Inject CLI arg manually so preload can read it
process.argv.push(`--userDataPath=${userDataPath}`);
}
// Start update checks

View File

@@ -4,31 +4,6 @@ require('./rt/electron-rt');
console.log('User Preload!');
const { contextBridge, shell, ipcRenderer } = require('electron');
const fs = require('fs');
const path = require('path');
// Grab `--userDataPath=...` from process arguments (injected from main process)
const userDataArg = process.argv.find((arg) =>
arg.startsWith('--userDataPath=')
);
const userDataPath = userDataArg?.split('=')[1] || '.';
// Define path to the wallet storage JSON file
const filePath = path.join(userDataPath, 'wallet-storage.json');
// Manual JSON storage functions
function readData() {
try {
const raw = fs.readFileSync(filePath, 'utf-8');
return JSON.parse(raw);
} catch {
return {};
}
}
function writeData(data) {
fs.writeFileSync(filePath, JSON.stringify(data, null, 2));
}
// Expose Electron API
contextBridge.exposeInMainWorld('electronAPI', {
@@ -50,18 +25,41 @@ contextBridge.exposeInMainWorld('electron', {
ipcRenderer.invoke('fs:selectAndZip', filePath),
});
// 👇 New: Expose walletStorage to the frontend
// Expose it
contextBridge.exposeInMainWorld('walletStorage', {
get: (key) => readData()[key],
set: (key, value) => {
const data = readData();
data[key] = value;
writeData(data);
get: async (key) => {
const raw = await ipcRenderer.invoke(
'walletStorage:read',
'wallet-storage.json'
);
const data = raw ? JSON.parse(raw) : {};
return data[key];
},
delete: (key) => {
const data = readData();
set: async (key, value) => {
const raw = await ipcRenderer.invoke(
'walletStorage:read',
'wallet-storage.json'
);
const data = raw ? JSON.parse(raw) : {};
data[key] = value;
await ipcRenderer.invoke(
'walletStorage:write',
'wallet-storage.json',
JSON.stringify(data, null, 2)
);
},
delete: async (key) => {
const raw = await ipcRenderer.invoke(
'walletStorage:read',
'wallet-storage.json'
);
const data = raw ? JSON.parse(raw) : {};
delete data[key];
writeData(data);
await ipcRenderer.invoke(
'walletStorage:write',
'wallet-storage.json',
JSON.stringify(data, null, 2)
);
},
});

View File

@@ -6,7 +6,17 @@ import {
} from '@capacitor-community/electron';
import chokidar from 'chokidar';
import type { MenuItemConstructorOptions } from 'electron';
import { app, BrowserWindow, Menu, MenuItem, nativeImage, Tray, session, ipcMain, dialog } from 'electron';
import {
app,
BrowserWindow,
Menu,
MenuItem,
nativeImage,
Tray,
session,
ipcMain,
dialog,
} from 'electron';
import electronIsDev from 'electron-is-dev';
import electronServe from 'electron-serve';
import windowStateKeeper from 'electron-window-state';
@@ -14,24 +24,24 @@ const AdmZip = require('adm-zip');
import { join } from 'path';
import { myCapacitorApp } from '.';
const fs = require('fs');
const path = require('path')
const path = require('path');
const defaultDomains = [
'capacitor-electron://-',
'http://127.0.0.1:12391',
'ws://127.0.0.1:12391',
'https://ext-node.qortal.link',
'wss://ext-node.qortal.link',
'https://appnode.qortal.org',
'wss://appnode.qortal.org',
"https://api.qortal.org",
"https://api2.qortal.org",
"https://apinode.qortalnodes.live",
"https://apinode1.qortalnodes.live",
"https://apinode2.qortalnodes.live",
"https://apinode3.qortalnodes.live",
"https://apinode4.qortalnodes.live",
"https://www.qort.trade"
'wss://ext-node.qortal.link',
'https://appnode.qortal.org',
'wss://appnode.qortal.org',
'https://api.qortal.org',
'https://api2.qortal.org',
'https://apinode.qortalnodes.live',
'https://apinode1.qortalnodes.live',
'https://apinode2.qortalnodes.live',
'https://apinode3.qortalnodes.live',
'https://apinode4.qortalnodes.live',
'https://www.qort.trade',
];
// let allowedDomains: string[] = [...defaultDomains]
@@ -44,7 +54,9 @@ const reloadWatcher = {
ready: false,
watcher: null,
};
export function setupReloadWatcher(electronCapacitorApp: ElectronCapacitorApp): void {
export function setupReloadWatcher(
electronCapacitorApp: ElectronCapacitorApp
): void {
reloadWatcher.watcher = chokidar
.watch(join(app.getAppPath(), 'app'), {
ignored: /[/\\]\./,
@@ -93,7 +105,9 @@ export class ElectronCapacitorApp {
) {
this.CapacitorFileConfig = capacitorFileConfig;
this.customScheme = this.CapacitorFileConfig.electron?.customUrlScheme ?? 'capacitor-electron';
this.customScheme =
this.CapacitorFileConfig.electron?.customUrlScheme ??
'capacitor-electron';
if (trayMenuTemplate) {
this.TrayMenuTemplate = trayMenuTemplate;
@@ -126,7 +140,11 @@ export class ElectronCapacitorApp {
async init(): Promise<void> {
const icon = nativeImage.createFromPath(
join(app.getAppPath(), 'assets', process.platform === 'win32' ? 'appIcon.ico' : 'appIcon.png')
join(
app.getAppPath(),
'assets',
process.platform === 'win32' ? 'appIcon.ico' : 'appIcon.png'
)
);
this.mainWindowState = windowStateKeeper({
defaultWidth: 1000,
@@ -141,23 +159,29 @@ export class ElectronCapacitorApp {
y: this.mainWindowState.y,
width: this.mainWindowState.width,
height: this.mainWindowState.height,
backgroundColor: '#27282c',
backgroundColor: '#27282c',
webPreferences: {
nodeIntegration: true,
contextIsolation: true,
// Use preload to inject the electron varriant overrides for capacitor plugins.
// preload: join(app.getAppPath(), "node_modules", "@capacitor-community", "electron", "dist", "runtime", "electron-rt.js"),
preload: preloadPath },
preload: preloadPath,
},
});
this.mainWindowState.manage(this.MainWindow);
if (this.CapacitorFileConfig.backgroundColor) {
this.MainWindow.setBackgroundColor(this.CapacitorFileConfig.electron.backgroundColor);
this.MainWindow.setBackgroundColor(
this.CapacitorFileConfig.electron.backgroundColor
);
}
// If we close the main window with the splashscreen enabled we need to destory the ref.
this.MainWindow.on('closed', () => {
if (this.SplashScreen?.getSplashWindow() && !this.SplashScreen.getSplashWindow().isDestroyed()) {
if (
this.SplashScreen?.getSplashWindow() &&
!this.SplashScreen.getSplashWindow().isDestroyed()
) {
this.SplashScreen.getSplashWindow().close();
}
});
@@ -186,11 +210,15 @@ export class ElectronCapacitorApp {
}
});
this.TrayIcon.setToolTip(app.getName());
this.TrayIcon.setContextMenu(Menu.buildFromTemplate(this.TrayMenuTemplate));
this.TrayIcon.setContextMenu(
Menu.buildFromTemplate(this.TrayMenuTemplate)
);
}
// Setup the main manu bar at the top of our window.
Menu.setApplicationMenu(Menu.buildFromTemplate(this.AppMenuBarMenuTemplate));
Menu.setApplicationMenu(
Menu.buildFromTemplate(this.AppMenuBarMenuTemplate)
);
// If the splashscreen is enabled, show it first while the main window loads then switch it out for the main window, or just load the main window from the start.
if (this.CapacitorFileConfig.electron?.splashScreenEnabled) {
@@ -198,7 +226,8 @@ export class ElectronCapacitorApp {
imageFilePath: join(
app.getAppPath(),
'assets',
this.CapacitorFileConfig.electron?.splashScreenImageName ?? 'splash.png'
this.CapacitorFileConfig.electron?.splashScreenImageName ??
'splash.png'
),
windowWidth: 400,
windowHeight: 400,
@@ -237,31 +266,36 @@ export class ElectronCapacitorApp {
if (electronIsDev) {
this.MainWindow.webContents.openDevTools();
}
CapElectronEventEmitter.emit('CAPELECTRON_DeeplinkListenerInitialized', '');
CapElectronEventEmitter.emit(
'CAPELECTRON_DeeplinkListenerInitialized',
''
);
}, 400);
});
}
}
export function setupContentSecurityPolicy(customScheme: string): void {
session.defaultSession.webRequest.onHeadersReceived((details: any, callback) => {
const allowedSources = ["'self'", customScheme, ...domainHolder.allowedDomains];
const frameSources = [
"'self'",
'http://localhost:*',
'https://localhost:*',
'ws://localhost:*',
'ws://127.0.0.1:*',
'http://127.0.0.1:*',
'https://127.0.0.1:*',
...allowedSources,
];
session.defaultSession.webRequest.onHeadersReceived(
(details: any, callback) => {
const allowedSources = [
"'self'",
customScheme,
...domainHolder.allowedDomains,
];
const frameSources = [
"'self'",
'http://localhost:*',
'https://localhost:*',
'ws://localhost:*',
'ws://127.0.0.1:*',
'http://127.0.0.1:*',
'https://127.0.0.1:*',
...allowedSources,
];
// Create the Content Security Policy (CSP) string
const csp = `
// Create the Content Security Policy (CSP) string
const csp = `
default-src 'self' ${frameSources.join(' ')};
frame-src ${frameSources.join(' ')};
script-src 'self' 'wasm-unsafe-eval' 'unsafe-inline' 'unsafe-eval' ${frameSources.join(' ')};
@@ -271,59 +305,57 @@ export function setupContentSecurityPolicy(customScheme: string): void {
media-src 'self' blob: ${frameSources.join(' ')};
style-src 'self' 'unsafe-inline';
font-src 'self' data:;
`.replace(/\s+/g, ' ').trim();
// Get the request URL and origin
const requestUrl = details.url;
const requestOrigin = details.origin || details.referrer || 'capacitor-electron://-';
`
.replace(/\s+/g, ' ')
.trim();
// Parse the request URL to get its origin
let requestUrlOrigin: string;
try {
const parsedUrl = new URL(requestUrl);
requestUrlOrigin = parsedUrl.origin;
} catch (e) {
// Handle invalid URLs gracefully
requestUrlOrigin = '';
// Get the request URL and origin
const requestUrl = details.url;
const requestOrigin =
details.origin || details.referrer || 'capacitor-electron://-';
// Parse the request URL to get its origin
let requestUrlOrigin: string;
try {
const parsedUrl = new URL(requestUrl);
requestUrlOrigin = parsedUrl.origin;
} catch (e) {
// Handle invalid URLs gracefully
requestUrlOrigin = '';
}
// Determine if the request is cross-origin
const isCrossOrigin = requestOrigin !== requestUrlOrigin;
// Check if the response already includes Access-Control-Allow-Origin
const hasAccessControlAllowOrigin = Object.keys(
details.responseHeaders
).some(
(header) => header.toLowerCase() === 'access-control-allow-origin'
);
// Prepare response headers
const responseHeaders: Record<string, string | string[]> = {
...details.responseHeaders,
'Content-Security-Policy': [csp],
};
if (isCrossOrigin && !hasAccessControlAllowOrigin) {
// Handle CORS for cross-origin requests lacking CORS headers
// Optionally, check if the requestOrigin is allowed
responseHeaders['Access-Control-Allow-Origin'] = requestOrigin;
responseHeaders['Access-Control-Allow-Methods'] =
'GET, POST, OPTIONS, DELETE';
responseHeaders['Access-Control-Allow-Headers'] =
'Content-Type, Authorization, x-api-key';
}
// Callback with modified headers
callback({ responseHeaders });
}
// Determine if the request is cross-origin
const isCrossOrigin = requestOrigin !== requestUrlOrigin;
// Check if the response already includes Access-Control-Allow-Origin
const hasAccessControlAllowOrigin = Object.keys(details.responseHeaders).some(
(header) => header.toLowerCase() === 'access-control-allow-origin'
);
// Prepare response headers
const responseHeaders: Record<string, string | string[]> = {
...details.responseHeaders,
'Content-Security-Policy': [csp],
};
if (isCrossOrigin && !hasAccessControlAllowOrigin) {
// Handle CORS for cross-origin requests lacking CORS headers
// Optionally, check if the requestOrigin is allowed
responseHeaders['Access-Control-Allow-Origin'] = requestOrigin;
responseHeaders['Access-Control-Allow-Methods'] = 'GET, POST, OPTIONS, DELETE';
responseHeaders['Access-Control-Allow-Headers'] = 'Content-Type, Authorization, x-api-key';
}
// Callback with modified headers
callback({ responseHeaders });
});
);
}
// IPC listener for updating allowed domains
ipcMain.on('set-allowed-domains', (event, domains: string[]) => {
if (!Array.isArray(domains)) {
@@ -331,20 +363,22 @@ ipcMain.on('set-allowed-domains', (event, domains: string[]) => {
}
// Validate and transform user-provided domains
const validatedUserDomains = domains
.flatMap((domain) => {
try {
const url = new URL(domain);
const protocol = url.protocol === 'https:' ? 'wss:' : 'ws:';
const socketUrl = `${protocol}//${url.hostname}${url.port ? ':' + url.port : ''}`;
return [url.origin, socketUrl];
} catch {
return [];
}
})
.filter(Boolean) as string[];
.flatMap((domain) => {
try {
const url = new URL(domain);
const protocol = url.protocol === 'https:' ? 'wss:' : 'ws:';
const socketUrl = `${protocol}//${url.hostname}${url.port ? ':' + url.port : ''}`;
return [url.origin, socketUrl];
} catch {
return [];
}
})
.filter(Boolean) as string[];
// Combine default and validated user domains
const newAllowedDomains = [...new Set([...defaultDomains, ...validatedUserDomains])];
const newAllowedDomains = [
...new Set([...defaultDomains, ...validatedUserDomains]),
];
// Sort both current allowed domains and new domains for comparison
const sortedCurrentDomains = [...domainHolder.allowedDomains].sort();
@@ -353,7 +387,9 @@ ipcMain.on('set-allowed-domains', (event, domains: string[]) => {
// Check if the lists are different
const hasChanged =
sortedCurrentDomains.length !== sortedNewDomains.length ||
sortedCurrentDomains.some((domain, index) => domain !== sortedNewDomains[index]);
sortedCurrentDomains.some(
(domain, index) => domain !== sortedNewDomains[index]
);
// If there's a change, update allowedDomains and reload the window
if (hasChanged) {
@@ -363,15 +399,14 @@ ipcMain.on('set-allowed-domains', (event, domains: string[]) => {
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.reload();
}
}
}
});
ipcMain.handle('dialog:openFile', async () => {
const result = await dialog.showOpenDialog({
properties: ['openFile'],
filters: [
{ name: 'ZIP Files', extensions: ['zip'] } // Restrict to ZIP files
{ name: 'ZIP Files', extensions: ['zip'] }, // Restrict to ZIP files
],
});
return result.filePaths[0];
@@ -389,9 +424,8 @@ ipcMain.handle('fs:readFile', async (_, filePath) => {
// Read the file as a Buffer
const fileBuffer = fs.readFileSync(absolutePath);
return fileBuffer
return fileBuffer;
} catch (error) {
console.error('Error reading file:', error.message);
return null; // Return null on error
@@ -399,38 +433,47 @@ ipcMain.handle('fs:readFile', async (_, filePath) => {
});
ipcMain.handle('fs:selectAndZip', async (_, path) => {
let directoryPath = path
if(!directoryPath){
let directoryPath = path;
if (!directoryPath) {
const { canceled, filePaths } = await dialog.showOpenDialog({
properties: ['openDirectory'],
});
if (canceled || filePaths.length === 0) {
console.log('No directory selected');
return null;
}
});
if (canceled || filePaths.length === 0) {
console.log('No directory selected');
return null;
}
directoryPath = filePaths[0];
}
try {
directoryPath = filePaths[0];
}
try {
// Add the entire directory to the zip
const zip = new AdmZip();
// Add the entire directory to the zip
zip.addLocalFolder(directoryPath);
// Add the entire directory to the zip
zip.addLocalFolder(directoryPath);
// Generate the zip file as a buffer
const zipBuffer = zip.toBuffer();
// Generate the zip file as a buffer
const zipBuffer = zip.toBuffer();
return {buffer: zipBuffer, directoryPath}
} catch (error) {
return null
}
return { buffer: zipBuffer, directoryPath };
} catch (error) {
return null;
}
});
ipcMain.handle('walletStorage:read', async (_event, fileName: string) => {
const filePath = path.join(app.getPath('userData'), fileName);
const exists = fs.existsSync(filePath);
if (!exists) return null;
return fs.promises.readFile(filePath, 'utf-8');
});
ipcMain.handle(
'walletStorage:write',
async (_event, fileName: string, data: string) => {
const filePath = path.join(app.getPath('userData'), fileName);
await fs.promises.writeFile(filePath, data, 'utf-8');
return true;
}
);

View File

@@ -1,36 +0,0 @@
// wallet-storage.ts
import fs from 'fs';
import path from 'path';
import { app } from 'electron';
const filePath = path.join(app.getPath('userData'), 'wallet-storage.json');
function readData(): Record<string, any> {
try {
const raw = fs.readFileSync(filePath, 'utf-8');
return JSON.parse(raw);
} catch {
return {};
}
}
function writeData(data: Record<string, any>) {
fs.writeFileSync(filePath, JSON.stringify(data, null, 2));
}
export function getValue<T = any>(key: string): T | undefined {
const data = readData();
return data[key];
}
export function setValue(key: string, value: any): void {
const data = readData();
data[key] = value;
writeData(data);
}
export function deleteValue(key: string): void {
const data = readData();
delete data[key];
writeData(data);
}

View File

@@ -4,27 +4,29 @@ export const OnLaunchWrapper = ({ children }) => {
const [isLoaded, setIsLoaded] = useState(false);
useEffect(() => {
try {
if (window.walletStorage) {
const res = window.walletStorage.get('apiKey');
if (res) {
window.sendMessage('setApiKey', res).finally(() => {
setTimeout(() => {
setIsLoaded(true);
}, 250);
});
const fetchApiKey = async () => {
try {
if (window.walletStorage) {
const res = await window.walletStorage.get('apiKey');
if (res) {
await window.sendMessage('setApiKey', res);
setTimeout(() => setIsLoaded(true), 250);
} else {
setIsLoaded(true);
}
} else {
setIsLoaded(true);
}
} else {
} catch (error) {
console.error(
'Error occurred when fetching apiKey info from file system',
error
);
setIsLoaded(true);
}
} catch (error) {
setIsLoaded(true);
console.error(
'Error has occured when fetching apiKey info from file system'
);
}
};
fetchApiKey();
}, []);
return !isLoaded ? null : children;
};

View File

@@ -954,7 +954,7 @@ export async function setApiKeyCase(request, event) {
const payload = request.payload;
if (window?.walletStorage) {
window.walletStorage.set('apiKey', payload);
await window.walletStorage.set('apiKey', payload);
}
storeData('apiKey', payload);
event.source.postMessage(
@@ -982,7 +982,7 @@ export async function setCustomNodesCase(request, event) {
try {
const nodes = request.payload;
if (window?.walletStorage) {
window.walletStorage.set('customNodes', nodes);
await window.walletStorage.set('customNodes', nodes);
} else {
storeData('customNodes', nodes);
}

View File

@@ -251,7 +251,7 @@ export const getApiKeyFromStorage = async (): Promise<string | null> => {
export const getCustomNodesFromStorage = async (): Promise<any | null> => {
if (window?.walletStorage) {
const res = window.walletStorage.get('customNodes');
const res = await window.walletStorage.get('customNodes');
return res || null;
} else {
@@ -868,11 +868,11 @@ export async function getSaveWallet() {
export async function getWallets() {
let res;
if (window?.walletStorage) {
res = window.walletStorage.get('wallets');
res = await window.walletStorage.get('wallets');
if (!res) {
const prevWallets = await getData<any>('wallets').catch(() => null);
if (prevWallets) {
window.walletStorage.set('wallets', prevWallets);
await window.walletStorage.set('wallets', prevWallets);
res = prevWallets;
}
}
@@ -889,7 +889,7 @@ export async function getWallets() {
export async function storeWallets(wallets) {
if (window?.walletStorage) {
window.walletStorage.set('wallets', wallets);
await window.walletStorage.set('wallets', wallets);
} else {
storeData('wallets', wallets).catch((error) => {
console.error(error);

View File

@@ -35,42 +35,49 @@ export const useHandleTutorials = () => {
const { t } = useTranslation(['core', 'tutorial']);
useEffect(() => {
try {
let storedData;
if (window?.walletStorage) {
storedData = window.walletStorage.get('shown-tutorials');
} else {
storedData = localStorage.getItem('shown-tutorials');
}
const fetchShownTutorials = async () => {
try {
let storedData: any;
if (storedData) {
setShowTutorials(JSON.parse(storedData));
} else {
if (window?.walletStorage) {
storedData = await window.walletStorage.get('shown-tutorials');
} else {
const local = localStorage.getItem('shown-tutorials');
storedData = local ? JSON.parse(local) : {};
}
setShowTutorials(storedData || {});
} catch (error) {
console.error('Failed to load tutorial state:', error);
setShowTutorials({});
}
} catch (error) {
//error
}
};
fetchShownTutorials();
}, []);
const saveShowTutorial = useCallback((type) => {
try {
setShowTutorials((prev) => {
const objectToSave = {
...(prev || {}),
const saveShowTutorial = useCallback(
async (type) => {
try {
const updated = {
...(shownTutorials || {}),
[type]: true,
};
setShowTutorials(updated);
if (window?.walletStorage) {
window.walletStorage.set('shown-tutorials', objectToSave);
await window.walletStorage.set('shown-tutorials', updated);
} else {
saveToLocalStorage('shown-tutorials', type, true);
}
return objectToSave;
});
} catch (error) {
//error
}
}, []);
} catch (error) {
console.error('Failed to save tutorial state:', error);
}
},
[shownTutorials]
);
const showTutorial = useCallback(
async (type, isForce) => {
try {