mirror of
https://github.com/pezkuwichain/pezkuwi-apps.git
synced 2026-04-29 00:07:56 +00:00
feat: initial Pezkuwi Apps rebrand from polkadot-apps
Rebranded terminology: - Polkadot → Pezkuwi - Kusama → Dicle - Westend → Zagros - Rococo → PezkuwiChain - Substrate → Bizinikiwi - parachain → teyrchain Custom logos with Kurdistan brand colors (#e6007a → #86e62a): - bizinikiwi-hexagon.svg - sora-bizinikiwi.svg - hezscanner.svg - heztreasury.svg - pezkuwiscan.svg - pezkuwistats.svg - pezkuwiassembly.svg - pezkuwiholic.svg
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
// Copyright 2017-2025 @pezkuwi/apps authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore Warned on by nodenext resolution (while package does build in bundler mode)
|
||||
import type { KeyringJson } from '@pezkuwi/ui-keyring/types';
|
||||
|
||||
export interface AccountStoreApi {
|
||||
all: () => Promise<{ key: string, value: KeyringJson }[]>
|
||||
get: (key: string) => Promise<KeyringJson>
|
||||
remove: (key: string) => Promise<void>
|
||||
set: (key: string, value: KeyringJson) => Promise<void>
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
// Copyright 2017-2025 @pezkuwi/apps authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { AccountStoreApi } from './account-store-api.js';
|
||||
|
||||
export interface ElectronMainApi {
|
||||
accountStore: AccountStoreApi
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
// Copyright 2017-2025 @pezkuwi/apps authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { ElectronMainApi } from './electron-main-api.js';
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
ElectronMain: ElectronMainApi
|
||||
}
|
||||
}
|
||||
|
||||
export const electronMainApi = window.ElectronMain;
|
||||
@@ -0,0 +1,18 @@
|
||||
// Copyright 2017-2025 @pezkuwi/apps authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { AppUpdater } from 'electron-updater';
|
||||
|
||||
export async function setupAutoUpdater (): Promise<void> {
|
||||
const { autoUpdater } = await import('electron-updater');
|
||||
|
||||
await setLogger(autoUpdater);
|
||||
autoUpdater.checkForUpdatesAndNotify().catch(console.error);
|
||||
}
|
||||
|
||||
async function setLogger (autoUpdater: AppUpdater): Promise<void> {
|
||||
const log = await import('electron-log');
|
||||
|
||||
log.transports.file.level = 'debug';
|
||||
autoUpdater.logger = log;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
// Copyright 2017-2025 @pezkuwi/apps authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { HeadersReceivedResponse } from 'electron';
|
||||
|
||||
import { session } from 'electron';
|
||||
|
||||
export function setupContentSecurityPolicy (_: string): void {
|
||||
session.defaultSession.webRequest.onHeadersReceived((details, respond: (response: HeadersReceivedResponse) => void) => {
|
||||
respond({
|
||||
responseHeaders: {
|
||||
...details.responseHeaders,
|
||||
'Content-Security-Policy': [
|
||||
"default-src 'self';" +
|
||||
" style-src-elem 'self' https://fonts.googleapis.com/css 'unsafe-inline';" +
|
||||
" font-src data: 'self' https://fonts.gstatic.com;" +
|
||||
" style-src 'unsafe-inline';" +
|
||||
" connect-src 'self' wss: ws:;" +
|
||||
" img-src 'self' data:;" +
|
||||
// react-qr-reader uses an embedded blob
|
||||
" worker-src 'self' blob: filesystem:;" +
|
||||
// unsafe-eval is needed for the WASM content - same as the extension
|
||||
// script hashes here are for the window.top script (not technically needed)
|
||||
" script-src 'self' 'unsafe-eval' 'sha256-02/ejyoV/iwRdJ4NAsxjzF6WVUtLMPM6Nv96EbAm6u8=' 'sha256-wW/WsLudCDaPo/ibpeK0KslHqYpCzcAKNFxFBXwCHJg='"
|
||||
]
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// Copyright 2017-2025 @pezkuwi/apps authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { app } from 'electron';
|
||||
|
||||
import { registerAccountStoreHandlers } from '../main/account-store.js';
|
||||
import { setupAutoUpdater } from './autoUpdater.js';
|
||||
import { setupContentSecurityPolicy } from './contentSecurityPolicy.js';
|
||||
import { createWindow } from './window.js';
|
||||
|
||||
const ENV = process.env.NODE_ENV || 'production';
|
||||
|
||||
app.on('web-contents-created', (_, webContents): void => {
|
||||
webContents.setWindowOpenHandler(() => ({ action: 'allow' }));
|
||||
});
|
||||
|
||||
app
|
||||
.whenReady()
|
||||
.then(async (): Promise<void> => {
|
||||
registerAccountStoreHandlers();
|
||||
setupContentSecurityPolicy(ENV);
|
||||
|
||||
await createWindow(ENV);
|
||||
await setupAutoUpdater();
|
||||
})
|
||||
.catch(console.error);
|
||||
@@ -0,0 +1,50 @@
|
||||
// Copyright 2017-2025 @pezkuwi/apps authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { BrowserWindow, screen, shell } from 'electron';
|
||||
import path from 'path';
|
||||
|
||||
export function createWindow (environment: string): Promise<unknown> {
|
||||
const { height, width } = screen.getPrimaryDisplay().workAreaSize;
|
||||
|
||||
const win = new BrowserWindow({
|
||||
height,
|
||||
icon: path.join(__dirname, 'icon.png'),
|
||||
webPreferences: {
|
||||
contextIsolation: true,
|
||||
nodeIntegration: false,
|
||||
preload: path.join(__dirname, 'preload.js')
|
||||
},
|
||||
width
|
||||
});
|
||||
|
||||
if (environment === 'development') {
|
||||
win.webContents.openDevTools();
|
||||
|
||||
return win.loadURL('http://127.0.0.1:3000/');
|
||||
}
|
||||
|
||||
// Handle attempts to open a new window via window.open()
|
||||
win.webContents.setWindowOpenHandler(({ url }) => {
|
||||
// Open all http/https URLs externally
|
||||
if (url.startsWith('http://') || url.startsWith('https://')) {
|
||||
shell.openExternal(url).catch(console.log);
|
||||
|
||||
return { action: 'deny' };
|
||||
}
|
||||
|
||||
return { action: 'allow' };
|
||||
});
|
||||
|
||||
// Handle in-app navigation attempts, such as clicking on <a href="...">
|
||||
win.webContents.on('will-navigate', (event, url) => {
|
||||
if (url.startsWith('http://') || url.startsWith('https://')) {
|
||||
event.preventDefault();
|
||||
shell.openExternal(url).catch(console.log);
|
||||
}
|
||||
});
|
||||
|
||||
const mainFilePath = path.resolve(__dirname, 'index.html');
|
||||
|
||||
return win.loadFile(mainFilePath);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
// Copyright 2017-2025 @pezkuwi/apps authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// setup these right at front
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore Warned on by nodenext resolution (while package does build in bundler mode)
|
||||
import '@pezkuwi/apps/initSettings';
|
||||
import 'semantic-ui-css/semantic.min.css';
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore Warned on by nodenext resolution (while package does build in bundler mode)
|
||||
import '@pezkuwi/react-components/i18n';
|
||||
|
||||
import React from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore Warned on by nodenext resolution (while package does build in bundler mode)
|
||||
import Root from '@pezkuwi/apps/Root';
|
||||
|
||||
import { electronMainApi } from './api/global-exported-api.js';
|
||||
import { RemoteElectronStore } from './renderer/remote-electron-store.js';
|
||||
|
||||
const rootId = 'root';
|
||||
const rootElement = document.getElementById(rootId);
|
||||
|
||||
if (!rootElement) {
|
||||
throw new Error(`Unable to find element with id '${rootId}'`);
|
||||
}
|
||||
|
||||
const store = new RemoteElectronStore(electronMainApi.accountStore);
|
||||
|
||||
createRoot(rootElement).render(
|
||||
<Root
|
||||
isElectron
|
||||
store={store}
|
||||
/>
|
||||
);
|
||||
@@ -0,0 +1,72 @@
|
||||
// Copyright 2017-2025 @pezkuwi/apps authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
/// <reference types="@pezkuwi/dev-test/globals.d.ts" />
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore Warned on by nodenext resolution (while package does build in bundler mode)
|
||||
import type { KeyringJson } from '@pezkuwi/ui-keyring/types';
|
||||
import type { IpcMainHandler } from './ipc-main-handler.js';
|
||||
|
||||
import * as tmp from 'tmp';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore Warned on by nodenext resolution (while package does build in bundler mode)
|
||||
import { FileStore } from '@pezkuwi/ui-keyring/stores';
|
||||
|
||||
import { accountStoreIpcHandler } from './account-store.js';
|
||||
|
||||
const exampleAccount = (address: string): KeyringJson => ({
|
||||
address,
|
||||
meta: {}
|
||||
});
|
||||
|
||||
describe('Account store', () => {
|
||||
let accountStore: IpcMainHandler;
|
||||
let tmpDir: tmp.DirResult;
|
||||
|
||||
beforeEach(() => {
|
||||
tmpDir = tmp.dirSync({ unsafeCleanup: true });
|
||||
accountStore = accountStoreIpcHandler(new FileStore(tmpDir.name));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
tmpDir.removeCallback();
|
||||
});
|
||||
|
||||
it('all returns empty array at first', () => {
|
||||
expect(accountStore['account-store-all']()).toEqual([]);
|
||||
});
|
||||
|
||||
it('after adding accounts, they become visible', async () => {
|
||||
await accountStore['account-store-set']('1', exampleAccount('a'));
|
||||
await accountStore['account-store-set']('2', exampleAccount('b'));
|
||||
|
||||
expect(accountStore['account-store-all']()).toEqual([{
|
||||
key: '1', value: exampleAccount('a')
|
||||
}, {
|
||||
key: '2', value: exampleAccount('b')
|
||||
}]);
|
||||
});
|
||||
|
||||
it('get returns account if exists', async () => {
|
||||
await accountStore['account-store-set']('1', exampleAccount('a'));
|
||||
expect(await accountStore['account-store-get']('1')).toEqual(exampleAccount('a'));
|
||||
});
|
||||
|
||||
it('get returns null if account does not exist', async () => {
|
||||
// jest.spyOn(console, 'error').mockImplementationOnce(() => { /**/ });
|
||||
|
||||
expect(await accountStore['account-store-get']('1')).toEqual(null);
|
||||
});
|
||||
|
||||
it('account disappears from list after it is removed', async () => {
|
||||
// jest.spyOn(console, 'error').mockImplementationOnce(() => { /**/ });
|
||||
|
||||
await accountStore['account-store-set']('1', exampleAccount('a'));
|
||||
await accountStore['account-store-remove']('1');
|
||||
|
||||
expect(await accountStore['account-store-get']('1')).toEqual(null);
|
||||
expect(accountStore['account-store-all']()).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,60 @@
|
||||
// Copyright 2017-2025 @pezkuwi/apps authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore Warned on by nodenext resolution (while package does build in bundler mode)
|
||||
import type { KeyringJson } from '@pezkuwi/ui-keyring/types';
|
||||
import type { IpcMainHandler } from './ipc-main-handler.js';
|
||||
|
||||
import electron from 'electron';
|
||||
import path from 'path';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore Warned on by nodenext resolution (while package does build in bundler mode)
|
||||
import { FileStore } from '@pezkuwi/ui-keyring/stores';
|
||||
|
||||
import { registerIpcHandler } from './register-ipc-handler.js';
|
||||
|
||||
const ACCOUNTS_SUBFOLDER = 'pezkuwi-accounts';
|
||||
|
||||
function safeWriteKey (key: string) {
|
||||
return key.replace(/:/g, '-');
|
||||
}
|
||||
|
||||
function safeReadKey (key: string) {
|
||||
return key.replace(/-/g, ':');
|
||||
}
|
||||
|
||||
export const accountStoreIpcHandler = (fileStore: FileStore): IpcMainHandler => ({
|
||||
'account-store-all': () => {
|
||||
let result: { key: string, value: KeyringJson }[] = [];
|
||||
|
||||
const collect = (key: string, value: KeyringJson) => {
|
||||
result = [...result, { key: safeReadKey(key), value }];
|
||||
};
|
||||
|
||||
fileStore.all(collect);
|
||||
|
||||
return result;
|
||||
},
|
||||
'account-store-get': async (key: string) => new Promise((resolve) => {
|
||||
try {
|
||||
fileStore.get(safeWriteKey(key), resolve);
|
||||
} catch {
|
||||
resolve(null);
|
||||
}
|
||||
}),
|
||||
'account-store-remove': async (key: string) => new Promise((resolve) =>
|
||||
fileStore.remove(safeWriteKey(key), () => resolve(undefined))
|
||||
),
|
||||
'account-store-set': async (key: string, value: KeyringJson) => new Promise((resolve) =>
|
||||
fileStore.set(safeWriteKey(key), value, () => resolve(undefined))
|
||||
)
|
||||
});
|
||||
|
||||
export const registerAccountStoreHandlers = (): void => {
|
||||
const defaultStorePath = path.join(electron.app.getPath('userData'), ACCOUNTS_SUBFOLDER);
|
||||
const fileStore = new FileStore(defaultStorePath);
|
||||
|
||||
registerIpcHandler(accountStoreIpcHandler(fileStore));
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
// Copyright 2017-2025 @pezkuwi/apps authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
export type IpcMainHandler = Record<string, (...args: any[]) => unknown>;
|
||||
@@ -0,0 +1,14 @@
|
||||
// Copyright 2017-2025 @pezkuwi/apps authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { IpcMainHandler } from './ipc-main-handler.js';
|
||||
|
||||
import electron from 'electron';
|
||||
|
||||
export const registerIpcHandler = (ipcHandler: IpcMainHandler): void => {
|
||||
for (const [channel, listener] of Object.entries(ipcHandler)) {
|
||||
electron.ipcMain.handle(channel, (_, ...args: unknown[]) => {
|
||||
return listener(...args);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
// Copyright 2017-2025 @pezkuwi/apps authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore Warned on by nodenext resolution (while package does build in bundler mode)
|
||||
import type { KeyringJson } from '@pezkuwi/ui-keyring/types';
|
||||
|
||||
import { contextBridge, ipcRenderer } from 'electron';
|
||||
|
||||
contextBridge.exposeInMainWorld('ElectronMain', {
|
||||
accountStore: {
|
||||
all: () => ipcRenderer.invoke('account-store-all'),
|
||||
get: (key: string) => ipcRenderer.invoke('account-store-get', key),
|
||||
remove: (key: string) => ipcRenderer.invoke('account-store-remove', key),
|
||||
set: (key: string, value: KeyringJson) => ipcRenderer.invoke('account-store-set', key, value)
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,95 @@
|
||||
// Copyright 2017-2025 @pezkuwi/apps authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
/// <reference types="@pezkuwi/dev-test/globals.d.ts" />
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore Warned on by nodenext resolution (while package does build in bundler mode)
|
||||
import type { KeyringJson } from '@pezkuwi/ui-keyring/types';
|
||||
|
||||
import { RemoteElectronStore } from './remote-electron-store.js';
|
||||
|
||||
describe('Remote Electron Store', () => {
|
||||
const accountStore = {
|
||||
all: jest.fn(),
|
||||
get: jest.fn(),
|
||||
remove: jest.fn(),
|
||||
set: jest.fn()
|
||||
};
|
||||
const remoteStore = new RemoteElectronStore(accountStore);
|
||||
|
||||
beforeEach(() => {
|
||||
accountStore.all.mockClear();
|
||||
accountStore.get.mockClear();
|
||||
accountStore.remove.mockClear();
|
||||
accountStore.set.mockClear();
|
||||
});
|
||||
|
||||
describe('all', () => {
|
||||
it('calls callback for each returned account', async () => {
|
||||
accountStore.all.mockResolvedValue([{
|
||||
key: 1,
|
||||
value: 'a'
|
||||
}, {
|
||||
key: 2,
|
||||
value: 'b'
|
||||
}]);
|
||||
const cb = jest.fn();
|
||||
|
||||
remoteStore.all(cb);
|
||||
await Promise.resolve();
|
||||
|
||||
expect(cb).toHaveBeenNthCalledWith(1, 1, 'a');
|
||||
expect(cb).toHaveBeenNthCalledWith(2, 2, 'b');
|
||||
});
|
||||
});
|
||||
|
||||
describe('get', () => {
|
||||
it('calls callback with returned account', async () => {
|
||||
accountStore.get.mockResolvedValue('a');
|
||||
const cb = jest.fn();
|
||||
|
||||
remoteStore.get('1', cb);
|
||||
await Promise.resolve();
|
||||
|
||||
expect(accountStore.get).toHaveBeenCalledWith('1');
|
||||
expect(cb).toHaveBeenCalledWith('a');
|
||||
});
|
||||
|
||||
it('calls callback with null if no accounts found', async () => {
|
||||
accountStore.get.mockResolvedValue(null);
|
||||
const cb = jest.fn();
|
||||
|
||||
remoteStore.get('1', cb);
|
||||
await Promise.resolve();
|
||||
|
||||
expect(cb).toHaveBeenCalledWith(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('remove', () => {
|
||||
it('calls callback after success', async () => {
|
||||
accountStore.remove.mockResolvedValue(null);
|
||||
const cb = jest.fn();
|
||||
|
||||
remoteStore.remove('1', cb);
|
||||
await Promise.resolve();
|
||||
|
||||
expect(accountStore.remove).toHaveBeenCalledWith('1');
|
||||
expect(cb).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('set', () => {
|
||||
it('calls callback after success', async () => {
|
||||
accountStore.set.mockResolvedValue(null);
|
||||
const cb = jest.fn();
|
||||
|
||||
remoteStore.set('1', 'a' as unknown as KeyringJson, cb);
|
||||
await Promise.resolve();
|
||||
|
||||
expect(accountStore.set).toHaveBeenCalledWith('1', 'a');
|
||||
expect(cb).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,42 @@
|
||||
// Copyright 2017-2025 @pezkuwi/apps authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore Warned on by nodenext resolution (while package does build in bundler mode)
|
||||
import type { KeyringJson, KeyringStore } from '@pezkuwi/ui-keyring/types';
|
||||
import type { AccountStoreApi } from '../api/account-store-api.js';
|
||||
|
||||
export class RemoteElectronStore implements KeyringStore {
|
||||
readonly #accountStore: AccountStoreApi;
|
||||
|
||||
constructor (accountStore: AccountStoreApi) {
|
||||
this.#accountStore = accountStore;
|
||||
}
|
||||
|
||||
all (cb: (key: string, value: KeyringJson) => void): void {
|
||||
this.#accountStore.all()
|
||||
.then((result: { key: string, value: KeyringJson }[]) => result?.forEach(({ key, value }) => cb(key, value)))
|
||||
.catch((e: Error) => {
|
||||
throw new Error(`error getting all accounts: ${e.message}`);
|
||||
});
|
||||
}
|
||||
|
||||
get (key: string, cb: (value: KeyringJson) => void): void {
|
||||
this.#accountStore.get(key)
|
||||
.then(cb).catch((e: Error) => {
|
||||
throw new Error(`error storing account: ${e.message}`);
|
||||
});
|
||||
}
|
||||
|
||||
remove (key: string, cb: (() => void) | undefined): void {
|
||||
this.#accountStore.remove(key).then(cb).catch((e: Error) => {
|
||||
throw new Error(`error removing account: ${e.message}`);
|
||||
});
|
||||
}
|
||||
|
||||
set (key: string, value: KeyringJson, cb: (() => void) | undefined): void {
|
||||
this.#accountStore.set(key, value).then(cb).catch((e: Error) => {
|
||||
throw new Error(`error saving account: ${e.message}`);
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user