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:
2026-01-07 13:05:27 +03:00
commit d21bfb1320
5867 changed files with 329019 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
build
node_modules
release
View File
View File
+17
View File
@@ -0,0 +1,17 @@
# @pezkuwi/apps-electron
Desktop Pezkuwi apps client for Windows, Mac and Linux.
## Installation
[Download here](https://github.com/pezkuwi-js/apps/releases/latest) latest versions for Windows, Mac and Linux.
## Development and testing
Contributions are welcome!
Follow steps described [here](https://github.com/pezkuwi-js/apps#development) to setup the project.
* Run `yarn start:electron` to start the app in development mode. You will possibly see the `Not Found / 404` message. It's ok, just wait for the build to finish and refresh pressing `Ctrl+R`.
* Run `yarn test` to run tests
* Run `yarn packElectron:(mac|linux|windows)` with the OS you want to build for to create the app executable. Find the packages in `packages/apps-electron/release`.
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<key>com.apple.security.cs.allow-dyld-environment-variables</key>
<true/>
<key>com.apple.security.device.camera</key>
<true/>
</dict>
</plist>
Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

@@ -0,0 +1,21 @@
---
productName: Polkadot-JS Apps
artifactName: Polkadot-JS-Apps-${version}.${ext}
files:
- "./build"
- "./assets"
- "./appleEntitlements"
appId: com.polkadotjs.polkadotjs-apps
linux:
executableName: polkadot-apps
mac:
artifactName: Polkadot-JS-Apps-mac-${version}.${ext}
category: public.app-category.finance
entitlements: "./packages/apps-electron/appleEntitlements/entitlements.mac.plist"
extendInfo:
NSCameraUsageDescription: "This app requires camera access to capture account data on imports"
hardenedRuntime: true
directories:
buildResources: "./assets"
output: "./release"
afterSign: electron-builder-notarize
+37
View File
@@ -0,0 +1,37 @@
{
"author": "Dijital Kurdistan Tech Institute <info@pezkuwichain.io>",
"bugs": "https://github.com/pezkuwichain/pezkuwi-apps/issues",
"description": "An Apps portal into the Pezkuwi network",
"engines": {
"node": ">=18"
},
"homepage": "https://github.com/pezkuwichain/pezkuwi-apps/tree/master/packages/apps-electron#readme",
"license": "Apache-2.0",
"name": "@pezkuwi/apps-electron",
"private": true,
"repository": {
"directory": "packages/apps-electron",
"type": "git",
"url": "https://github.com/pezkuwichain/pezkuwi-apps.git"
},
"sideEffects": false,
"version": "0.168.2-4-x",
"main": "build/electron.js",
"dependencies": {
"electron-log": "^5.0.1",
"electron-updater": "^6.1.7"
},
"devDependencies": {
"@pezkuwi/dev": "^0.85.2",
"@types/tmp": "^0.2.6",
"copy-webpack-plugin": "^11.0.0",
"electron": "28.0.0",
"electron-builder": "24.10.0",
"electron-builder-notarize": "^1.5.1",
"html-webpack-plugin": "^5.5.4",
"tmp": "^0.2.1"
},
"peerDependencies": {
"webpack": "*"
}
}
@@ -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);
}
+37
View File
@@ -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);
});
}
};
+17
View File
@@ -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}`);
});
}
}
@@ -0,0 +1,17 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"baseUrl": "..",
"outDir": "./build",
"rootDir": "./src",
/* Since this is a forced-by-electron-CJS-module, we have to switch this off */
"verbatimModuleSyntax": false
},
"exclude": [
"webpack.*.cjs"
],
"references": [
{ "path": "../apps/tsconfig.build.json" },
{ "path": "../react-components/tsconfig.build.json" }
]
}
+64
View File
@@ -0,0 +1,64 @@
// Copyright 2017-2025 @polkadot/apps authors & contributors
// SPDX-License-Identifier: Apache-2.0
/* eslint-disable camelcase */
const CopyWebpackPlugin = require('copy-webpack-plugin');
const path = require('path');
function createWebpack () {
return [
{
entry: {
electron: './src/electron',
preload: './src/preload.ts'
},
mode: 'production',
module: {
rules: [
{
include: /node_modules/,
test: /\.mjs$/,
type: 'javascript/auto'
},
{
exclude: /(node_modules)/,
test: /\.(ts|tsx)$/,
use: [
{
loader: require.resolve('ts-loader'),
options: {
configFile: 'tsconfig.webpack.json',
transpileOnly: true
}
}
]
}
]
},
node: {
__dirname: false,
__filename: false
},
output: {
filename: '[name].js',
path: path.join(__dirname, '/build')
},
plugins: [
new CopyWebpackPlugin({ patterns: [{ from: 'assets' }] })
],
resolve: {
alias: {
'@polkadot/hw-ledger-transports': require.resolve('@polkadot/hw-ledger-transports/node')
},
extensionAlias: {
'.js': ['.ts', '.tsx', '.js']
},
extensions: ['.js', '.jsx', '.json', '.mjs', '.ts', '.tsx']
},
target: 'electron-main'
}
];
}
module.exports = createWebpack();
@@ -0,0 +1,37 @@
// Copyright 2017-2025 @polkadot/apps authors & contributors
// SPDX-License-Identifier: Apache-2.0
/* eslint-disable camelcase */
const CopyWebpackPlugin = require('copy-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const path = require('path');
const { merge } = require('webpack-merge');
const baseConfig = require('../apps/webpack.base.cjs');
const context = __dirname;
module.exports = merge(
baseConfig(context, 'development'),
{
plugins: [
// It must be placed before HtmlWebpackPlugin
new CopyWebpackPlugin({
patterns: [{
from: '../apps/public',
globOptions: {
dot: true,
ignore: ['**/index.html']
}
}]
}),
new HtmlWebpackPlugin({
PAGE_TITLE: 'Polkadot/Substrate Portal',
minify: false,
template: path.join(context, '../apps/public/index.html')
})
],
target: 'web'
}
);