Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
f843539
dropdown start
Sep 6, 2025
dcbc75e
fix desktop compilation errors
Sep 6, 2025
5e316d4
Merge branch 'updatedesktop' into topbarelectron
Sep 6, 2025
849656d
Merge branch 'master' of https://github.com/remix-project-org/remix-p…
Sep 17, 2025
fc14481
electron dropdown done
Sep 17, 2025
58e2d65
timing issues
Sep 17, 2025
bb87687
fix buttons git
Sep 17, 2025
a829ebf
fix key errors
Sep 17, 2025
39e5268
open in finder
Sep 17, 2025
8c7de9c
home tab
Sep 17, 2025
63ba752
fix button
Sep 17, 2025
852119d
fs watchers
Sep 17, 2025
f70d545
Merge branch 'master' into topbarelectron
bunsenstraat Sep 17, 2025
4f21f43
Merge branch 'master' of https://github.com/remix-project-org/remix-p…
Oct 17, 2025
d5d8df9
track event
Oct 17, 2025
c627b37
tracking
Oct 17, 2025
6ffe2c9
lint & click
Oct 17, 2025
8770e52
update package version
Oct 17, 2025
afc4c39
Merge branch 'master' into topbarelectron
bunsenstraat Oct 17, 2025
a459cdd
disable dialog
Oct 17, 2025
5dd37a1
test runner
Oct 17, 2025
ebf3f6d
Merge branch 'topbarelectron' of https://github.com/remix-project-org…
Oct 17, 2025
d59bc92
click
Oct 17, 2025
7c2abed
metamask
Oct 18, 2025
7882884
gh tests fix
Oct 18, 2025
abdf60b
nuke tooltips
Oct 18, 2025
24e9ca1
Merge branch 'master' into topbarelectron
bunsenstraat Oct 21, 2025
f008286
Merge branch 'master' into topbarelectron
bunsenstraat Oct 23, 2025
5f290e7
patch verification
Oct 24, 2025
315cf2b
welcome to remix
Oct 24, 2025
f34aea8
zoom really small on e2e
Oct 25, 2025
ee5d467
try again
Oct 25, 2025
91e5ad5
set normal res
Oct 25, 2025
00a866f
Remove commented-out HomeTabFile component
bunsenstraat Oct 28, 2025
820c888
Merge branch 'master' into topbarelectron
bunsenstraat Oct 28, 2025
b9de7c4
Merge branch 'master' into topbarelectron
bunsenstraat Oct 28, 2025
4771e05
fix lint
bunsenstraat Oct 28, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 39 additions & 21 deletions apps/remix-ide-e2e/src/commands/hideToolTips.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,49 @@ import { NightwatchBrowser } from 'nightwatch'
import EventEmitter from 'events'

class HideToolTips extends EventEmitter {
command(this: NightwatchBrowser) {
console.log('Hiding tooltips...')
command(this: NightwatchBrowser): NightwatchBrowser {
const browser = this.api
browser
.perform((done) => {
browser.execute(function () {
// hide tooltips
function addStyle(styleString) {
const style = document.createElement('style')
style.textContent = styleString
document.head.append(style)
.execute(function () {
// Set global flag to disable all CustomTooltip components
(window as any).REMIX_DISABLE_TOOLTIPS = true

// Dispatch custom event to notify all CustomTooltip components
const event = new CustomEvent('remix-tooltip-toggle', {
detail: { disabled: true }
})
window.dispatchEvent(event)

// Add CSS as backup for any non-CustomTooltip tooltips
const style = document.createElement('style')
style.id = 'nightwatch-disable-tooltips'
style.textContent = `
.tooltip,
.popover,
[role="tooltip"],
[id*="Tooltip"],
[id*="tooltip"] {
display: none !important;
visibility: hidden !important;
opacity: 0 !important;
pointer-events: none !important;
}
addStyle(`
.popover {
display:none !important;
}
#scamDetails {
display:none !important;
}
`)
}, [], done())
})
.perform((done) => {
done()
`
const existing = document.getElementById('nightwatch-disable-tooltips')
if (existing) existing.remove()
document.head.appendChild(style)

// Remove any existing tooltips from DOM
document.querySelectorAll('.tooltip, .popover, [role="tooltip"]').forEach(el => {
try { el.remove() } catch (e) {}
})
}, [])
.pause(100)
.perform(() => {
this.emit('complete')
})

return browser
}
}

Expand Down
10 changes: 10 additions & 0 deletions apps/remix-ide/src/app/matomo/MatomoAutoInit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,16 @@ export async function autoInitializeMatomo(options: MatomoAutoInitOptions): Prom
}

try {
// Check for Electron - always initialize in anonymous mode (no consent needed)
const isElectron = (window as any).electronAPI !== undefined;
if (isElectron) {
log('Electron detected, auto-initializing in anonymous mode (server-side tracking)');
await matomoManager.initialize('anonymous');
await matomoManager.processPreInitQueue();
log('Electron Matomo initialized and pre-init queue processed');
return true;
}

// Check if we should show the consent dialog
const shouldShowDialog = matomoManager.shouldShowConsentDialog(config);

Expand Down
54 changes: 36 additions & 18 deletions apps/remix-ide/src/app/matomo/MatomoConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
* Single source of truth for Matomo site IDs and configuration
*/

import isElectron from 'is-electron';
import { MatomoConfig } from './MatomoManager';

// ================ DEVELOPER CONFIGURATION ================
Expand Down Expand Up @@ -34,7 +35,7 @@ export interface DomainCustomDimensions {
}

// Type for domain keys (single source of truth)
export type MatomotDomain = 'alpha.remix.live' | 'beta.remix.live' | 'remix.ethereum.org' | 'localhost' | '127.0.0.1';
export type MatomotDomain = 'alpha.remix.live' | 'beta.remix.live' | 'remix.ethereum.org' | 'localhost' | '127.0.0.1' | 'electron';

// Type for site ID configuration
export type SiteIdConfig = Record<MatomotDomain, number>;
Expand All @@ -54,7 +55,8 @@ export const MATOMO_DOMAINS: SiteIdConfig = {
'beta.remix.live': 2,
'remix.ethereum.org': 3,
'localhost': 5,
'127.0.0.1': 5
'127.0.0.1': 5,
'electron': 4 // Remix Desktop (Electron) app
};

// Bot tracking site IDs (separate databases to avoid polluting human analytics)
Expand All @@ -64,7 +66,8 @@ export const MATOMO_BOT_SITE_IDS: BotSiteIdConfig = {
'beta.remix.live': null, // TODO: Create bot tracking site in Matomo (e.g., site ID 11)
'remix.ethereum.org': 8, // TODO: Create bot tracking site in Matomo (e.g., site ID 12)
'localhost': 7, // Keep bots in same localhost site for testing (E2E tests need cookies)
'127.0.0.1': 7 // Keep bots in same localhost site for testing (E2E tests need cookies)
'127.0.0.1': 7, // Keep bots in same localhost site for testing (E2E tests need cookies)
'electron': null // Electron app uses same site ID for bots (filtered via isBot dimension)
};

// Domain-specific custom dimension IDs for HUMAN traffic
Expand Down Expand Up @@ -96,6 +99,12 @@ export const MATOMO_CUSTOM_DIMENSIONS: CustomDimensionsConfig = {
trackingMode: 1, // Dimension for 'anon'/'cookie' tracking mode
clickAction: 3, // Dimension for 'true'/'false' click tracking
isBot: 4 // Dimension for 'human'/'bot'/'automation' detection
},
// Electron Desktop App
electron: {
trackingMode: 1, // Dimension for 'anon'/'cookie' tracking mode
clickAction: 2, // Dimension for 'true'/'false' click tracking
isBot: 3 // Dimension for 'human'/'bot'/'automation' detection
}
};

Expand All @@ -119,25 +128,40 @@ export const MATOMO_BOT_CUSTOM_DIMENSIONS: BotCustomDimensionsConfig = {
trackingMode: 1,
clickAction: 3,
isBot: 2
}
},
'electron': null // Electron app uses same custom dimensions as human traffic
};

/**
* Get the appropriate domain key for tracking
* Returns 'electron' for Electron app, otherwise returns the hostname
*/
export function getDomainKey(): MatomotDomain {
if (isElectron()) {
return 'electron';
}

const hostname = window.location.hostname as MatomotDomain;
// Return hostname if it's a known domain, otherwise default to localhost
return MATOMO_DOMAINS[hostname] !== undefined ? hostname : 'localhost';
}

/**
* Get the appropriate site ID for the current domain and bot status
*
* @param isBot - Whether the visitor is detected as a bot
* @returns Site ID to use for tracking
*/
export function getSiteIdForTracking(isBot: boolean): number {
const hostname = window.location.hostname;
const domainKey = getDomainKey();

// If bot and bot site ID is configured, use it
if (isBot && MATOMO_BOT_SITE_IDS[hostname] !== null && MATOMO_BOT_SITE_IDS[hostname] !== undefined) {
return MATOMO_BOT_SITE_IDS[hostname];
if (isBot && MATOMO_BOT_SITE_IDS[domainKey] !== null && MATOMO_BOT_SITE_IDS[domainKey] !== undefined) {
return MATOMO_BOT_SITE_IDS[domainKey];
}

// Otherwise use normal site ID
return MATOMO_DOMAINS[hostname] || MATOMO_DOMAINS['localhost'];
return MATOMO_DOMAINS[domainKey];
}

/**
Expand All @@ -146,21 +170,15 @@ export function getSiteIdForTracking(isBot: boolean): number {
* @param isBot - Whether the visitor is detected as a bot (to use bot-specific dimensions if configured)
*/
export function getDomainCustomDimensions(isBot: boolean = false): DomainCustomDimensions {
const hostname = window.location.hostname;
const domainKey = getDomainKey();

// If bot and bot-specific dimensions are configured, use them
if (isBot && MATOMO_BOT_CUSTOM_DIMENSIONS[hostname] !== null && MATOMO_BOT_CUSTOM_DIMENSIONS[hostname] !== undefined) {
return MATOMO_BOT_CUSTOM_DIMENSIONS[hostname];
if (isBot && MATOMO_BOT_CUSTOM_DIMENSIONS[domainKey] !== null && MATOMO_BOT_CUSTOM_DIMENSIONS[domainKey] !== undefined) {
return MATOMO_BOT_CUSTOM_DIMENSIONS[domainKey];
}

// Return dimensions for current domain
if (MATOMO_CUSTOM_DIMENSIONS[hostname]) {
return MATOMO_CUSTOM_DIMENSIONS[hostname];
}

// Fallback to localhost if domain not found
console.warn(`No custom dimensions found for domain: ${hostname}, using localhost fallback`);
return MATOMO_CUSTOM_DIMENSIONS['localhost'];
return MATOMO_CUSTOM_DIMENSIONS[domainKey];
}

/**
Expand Down
84 changes: 64 additions & 20 deletions apps/remix-ide/src/app/matomo/MatomoManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,14 @@ export class MatomoManager implements IMatomoManager {
this.emit('log', { message, data, timestamp });
}

/**
* Check if running in Electron environment
*/
private isElectronApp(): boolean {
return typeof window !== 'undefined' &&
(window as any).electronAPI !== undefined;
}

private setupPaqInterception(): void {
this.log('Setting up _paq interception');
if (typeof window === 'undefined') return;
Expand Down Expand Up @@ -825,40 +833,63 @@ export class MatomoManager implements IMatomoManager {
trackEvent(eventObjOrCategory: MatomoEvent | string, action?: string, name?: string, value?: string | number): number {
const eventId = ++this.state.lastEventId;

// Extract event parameters
let category: string;
let eventAction: string;
let eventName: string | undefined;
let eventValue: string | number | undefined;
let isClick: boolean | undefined;

// If first parameter is a MatomoEvent object, use type-safe approach
if (typeof eventObjOrCategory === 'object' && eventObjOrCategory !== null && 'category' in eventObjOrCategory) {
const { category, action: eventAction, name: eventName, value: eventValue, isClick } = eventObjOrCategory;
category = eventObjOrCategory.category;
eventAction = eventObjOrCategory.action;
eventName = eventObjOrCategory.name;
eventValue = eventObjOrCategory.value;
isClick = eventObjOrCategory.isClick;

this.log(`Tracking type-safe event ${eventId}: ${category} / ${eventAction} / ${eventName} / ${eventValue} / isClick: ${isClick}`);
} else {
// Legacy string-based approach
category = eventObjOrCategory as string;
eventAction = action!;
eventName = name;
eventValue = value;

// Set custom action dimension for click tracking
if (isClick !== undefined) {
window._paq.push(['setCustomDimension', this.customDimensions.clickAction, isClick ? 'true' : 'false']);
}
this.log(`Tracking legacy event ${eventId}: ${category} / ${eventAction} / ${eventName} / ${eventValue} (⚠️ no click dimension)`);
}

const matomoEvent: MatomoCommand = ['trackEvent', category, eventAction];
if (eventName !== undefined) matomoEvent.push(eventName);
if (eventValue !== undefined) matomoEvent.push(eventValue);
// Check if running in Electron - use IPC bridge instead of _paq
if (this.isElectronApp()) {
this.log(`Electron detected - routing event through IPC bridge`);

window._paq.push(matomoEvent);
this.emit('event-tracked', { eventId, category, action: eventAction, name: eventName, value: eventValue, isClick });
const electronAPI = (window as any).electronAPI;
if (electronAPI && electronAPI.trackEvent) {
// Pass isClick as the 6th parameter
const eventData = ['trackEvent', category, eventAction, eventName || '', eventValue, isClick];
electronAPI.trackEvent(eventData).catch((err: any) => {
console.error('[Matomo] Failed to send event to Electron:', err);
});
}

this.emit('event-tracked', { eventId, category, action: eventAction, name: eventName, value: eventValue, isClick });
return eventId;
}

// Legacy string-based approach - no isClick dimension set
const category = eventObjOrCategory as string;
this.log(`Tracking legacy event ${eventId}: ${category} / ${action} / ${name} / ${value} (⚠️ no click dimension)`);
// Standard web tracking using _paq
if (isClick !== undefined) {
window._paq.push(['setCustomDimension', this.customDimensions.clickAction, isClick ? 'true' : 'false']);
}

const matomoEvent: MatomoCommand = ['trackEvent', category, action!];
if (name !== undefined) matomoEvent.push(name);
if (value !== undefined) matomoEvent.push(value);
const matomoEvent: MatomoCommand = ['trackEvent', category, eventAction];
if (eventName !== undefined) matomoEvent.push(eventName);
if (eventValue !== undefined) matomoEvent.push(eventValue);

window._paq.push(matomoEvent);
this.emit('event-tracked', { eventId, category, action, name, value });
this.emit('event-tracked', { eventId, category, action: eventAction, name: eventName, value: eventValue, isClick });

return eventId;
}

trackPageView(title?: string): void {
this.log(`Tracking page view: ${title || 'default'}`);
const pageView: MatomoCommand = ['trackPageView'];
Expand Down Expand Up @@ -948,6 +979,14 @@ export class MatomoManager implements IMatomoManager {
// ================== SCRIPT LOADING ==================

async loadScript(): Promise<void> {
// Skip script loading in Electron - we use IPC bridge instead
if (this.isElectronApp()) {
this.log('Electron detected - skipping Matomo script load (using IPC bridge)');
this.state.scriptLoaded = true;
this.emit('script-loaded');
return;
}

if (this.state.scriptLoaded) {
this.log('Script already loaded');
return;
Expand Down Expand Up @@ -1344,11 +1383,16 @@ export class MatomoManager implements IMatomoManager {
*/
shouldShowConsentDialog(configApi?: any): boolean {
try {
// Electron doesn't need cookie consent (uses server-side HTTP tracking)
const isElectron = (window as any).electronAPI !== undefined;
if (isElectron) {
return false;
}

// Use domains from constructor config or fallback to empty object
const matomoDomains = this.config.matomoDomains || {};

const isElectron = (window as any).electronAPI !== undefined;
const isSupported = matomoDomains[window.location.hostname] || isElectron;
const isSupported = matomoDomains[window.location.hostname];

if (!isSupported) {
return false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export class compilerLoaderPlugin extends Plugin {
export class compilerLoaderPluginDesktop extends ElectronPlugin {
constructor() {
super(profile)
this.methods = []
this.methods = methods
}

async onActivation(): Promise<void> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@ export class electronConfig extends ElectronPlugin {
name: 'electronconfig',
description: 'electronconfig',
})
this.methods = []
this.methods = ['readConfig']
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -262,10 +262,10 @@ export class TemplatesSelectionPlugin extends ViewPlugin {
item.templateType = TEMPLATE_METADATA[item.value]

if (item.templateType && item.templateType.desktopCompatible === false && isElectron()) {
return (<></>)
return <React.Fragment key={item.name || index}></React.Fragment>
}

if (item.templateType && item.templateType.disabled === true) return
if (item.templateType && item.templateType.disabled === true) return null
if (!item.opts) {
return (
<RemixUIGridCell
Expand Down
6 changes: 3 additions & 3 deletions apps/remixdesktop/TEST.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@

### Executables

Testing runs through nightwatch that treats an electron application as a special version of chrome, which it is. It basically calls the executable which is built by the ./rundist.bash script which creates executables based on the current channel configuration package.json, ie "version": "1.0.8-insiders"
Testing runs through nightwatch that treats an electron application as a special version of chrome, which it is. It basically calls the executable which is built by the yarn dist -c test_only.json script which creates executables based on the current channel configuration package.json, ie "version": "1.0.8-insiders"

Executables are stored in the ./release directory. Without that executable you cannot run tests. You cannot call tests on local development instance that is launched by yarn start:dev. You need to create an executable file first.

This is done by running ./rundist.bash
This is done by running yarn dist -c test_only.json

Normally when you would do a 'real' release you would package remix IDE into the distributable but for local e2e this is not necessary because it will use the remix IDE that is being served.

Expand All @@ -40,7 +40,7 @@ In order to facilitate local testing nightwatch will boot the executable with th
So to start testing locally
- run the IDE with 'yarn serve' as you would normally do.
- build your release. You will always need to do this when the Desktop app itself changes its code.
- ./rundist.bash
- yarn dist -c test_only.json
- in apps/remixdesktop:
- yarn build:e2e
- run the test you want, refer to the actual JS that is build, not the TS, ie
Expand Down
Loading