Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(keyAutoAdd): show notification with connect progress #634

Merged
merged 6 commits into from
Oct 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ Inside this project, you'll see the following folders and files:
│ ├── content/ # Source code for the content scripts
│ │ └── keyAutoAdd/ # content scripts for automatic key addition to wallets
│ ├── popup/ # Source code for the popup UI
│ ├── pages/ # Source code for additional extension pages
│ ├── shared/ # Shared utilities
│ └── manifest.json # Extension's manifest - processed by Webpack depending on the target build
├── jest.config.ts
Expand Down
4 changes: 4 additions & 0 deletions esbuild/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ export const options: BuildOptions = {
in: path.join(SRC_DIR, 'popup', 'index.tsx'),
out: path.join('popup', 'popup'),
},
{
in: path.join(SRC_DIR, 'pages', 'progress-connect', 'index.tsx'),
out: path.join('pages', 'progress-connect', 'progress-connect'),
},
],
bundle: true,
legalComments: 'none',
Expand Down
21 changes: 21 additions & 0 deletions esbuild/dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,20 @@ function liveReloadPlugin({ target }: { target: Target }): ESBuildPlugin {
}
);`;

const reloadScriptPages = `
new EventSource("http://localhost:${port}/esbuild").addEventListener(
"change",
(ev) => {
const data = JSON.parse(ev.data);
if (
data.added.some(s => s.includes("/pages/")) ||
data.updated.some(s => s.includes("/pages/"))
) {
globalThis.location.reload();
}
}
);`;

const reloadScriptContent = `
new EventSource("http://localhost:${port}/esbuild").addEventListener(
"change",
Expand Down Expand Up @@ -95,6 +109,13 @@ function liveReloadPlugin({ target }: { target: Target }): ESBuildPlugin {
loader: 'tsx' as const,
};
});
build.onLoad({ filter: /src\/pages\/.+\/index.tsx$/ }, async (args) => {
const contents = await readFile(args.path, 'utf8');
return {
contents: contents + '\n\n\n' + reloadScriptPages,
loader: 'tsx' as const,
};
});
build.onLoad({ filter: /src\/content\// }, async (args) => {
const contents = await readFile(args.path, 'utf8');
return {
Expand Down
4 changes: 4 additions & 0 deletions esbuild/plugins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ export const getPlugins = ({
from: path.join(SRC_DIR, 'popup', 'index.html'),
to: path.join(outDir, 'popup', 'index.html'),
},
{
from: path.join(SRC_DIR, 'pages', 'progress-connect', 'index.html'),
to: path.join(outDir, 'pages', 'progress-connect', 'index.html'),
},
{
from: path.join(SRC_DIR, '_locales/**/*'),
to: path.join(outDir, '_locales'),
Expand Down
10 changes: 8 additions & 2 deletions src/background/services/keyAutoAdd.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,25 +89,29 @@ export class KeyAutoAddService {
};
this.browser.tabs.onRemoved.addListener(onTabCloseListener);

const ports = new Set<Runtime.Port>();
const onConnectListener: OnConnectCallback = (port) => {
if (port.name !== CONNECTION_NAME) return;
if (port.error) {
reject(new Error(port.error.message));
return;
}
ports.add(port);

port.postMessage({ action: 'BEGIN', payload });

port.onMessage.addListener(onMessageListener);

port.onDisconnect.addListener(() => {
ports.delete(port);
// wait for connect again so we can send message again if not connected,
// and not errored already (e.g. page refreshed)
});
};

const onMessageListener: OnPortMessageListener = (
message: KeyAutoAddToBackgroundMessage,
port,
) => {
if (message.action === 'SUCCESS') {
this.browser.runtime.onConnect.removeListener(onConnectListener);
Expand All @@ -124,8 +128,10 @@ export class KeyAutoAddService {
]),
);
} else if (message.action === 'PROGRESS') {
// can save progress to show in popup
// console.table(message.payload.steps);
// can also save progress to show in popup
for (const p of ports) {
if (p !== port) p.postMessage(message);
}
} else {
reject(new Error(`Unexpected message: ${JSON.stringify(message)}`));
}
Expand Down
91 changes: 89 additions & 2 deletions src/content/keyAutoAdd/lib/keyAutoAdd.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
// cSpell:ignore allowtransparency
import browser, { type Runtime } from 'webextension-polyfill';
import { CONNECTION_NAME } from '@/background/services/keyAutoAdd';
import {
errorWithKeyToJSON,
isErrorWithKey,
sleep,
withResolvers,
type ErrorWithKeyLike,
} from '@/shared/helpers';
import type {
Expand All @@ -24,6 +27,7 @@ const SYMBOL_SKIP = Symbol.for('skip');

export class KeyAutoAdd {
private port: Runtime.Port;
private ui: HTMLIFrameElement;

private stepsInput: Map<string, Step>;
private steps: StepWithStatus[];
Expand All @@ -45,6 +49,78 @@ export class KeyAutoAdd {
);
}

private setNotificationSize(size: 'notification' | 'fullscreen' | 'hidden') {
let styles: Partial<CSSStyleDeclaration>;
const defaultStyles: Partial<CSSStyleDeclaration> = {
outline: 'none',
border: 'none',
zIndex: '9999',
position: 'fixed',
top: '0',
left: '0',
};

if (size === 'notification') {
styles = {
width: '22rem',
height: '8rem',
position: 'fixed',
top: '1rem',
right: '1rem',
left: 'initial',
boxShadow: 'rgba(0, 0, 0, 0.1) 0px 0px 6px 3px',
borderRadius: '0.5rem',
};
} else if (size === 'fullscreen') {
styles = {
width: '100vw',
height: '100vh',
};
} else {
styles = {
width: '0',
height: '0',
position: 'absolute',
};
}

this.ui.style.cssText = '';
raducristianpopa marked this conversation as resolved.
Show resolved Hide resolved
Object.assign(this.ui.style, defaultStyles);
Object.assign(this.ui.style, styles);

const iframeUrl = new URL(
browser.runtime.getURL('pages/progress-connect/index.html'),
);
const params = new URLSearchParams({ mode: size });
iframeUrl.hash = '?' + params.toString();
if (this.ui.src !== iframeUrl.href && size !== 'hidden') {
this.ui.src = iframeUrl.href;
}
}

private addNotification() {
const { resolve, reject, promise } = withResolvers<void>();
if (this.ui) {
resolve();
return promise;
}
const pageUrl = browser.runtime.getURL('pages/progress-connect/index.html');
const iframe = document.createElement('iframe');
iframe.setAttribute('allowtransparency', 'true');
iframe.src = pageUrl;
document.body.appendChild(iframe);
iframe.addEventListener('load', () => {
resolve();
sleep(500).then(() =>
this.postMessage('PROGRESS', { steps: this.steps }),
);
});
iframe.addEventListener('error', reject, { once: true });
raducristianpopa marked this conversation as resolved.
Show resolved Hide resolved
this.ui = iframe;
this.setNotificationSize('hidden');
return promise;
}

private async run({
walletAddressUrl,
publicKey,
Expand All @@ -64,13 +140,24 @@ export class KeyAutoAdd {
details: typeof details === 'string' ? new Error(details) : details,
};
},
setNotificationSize: (size: 'notification' | 'fullscreen') => {
this.setNotificationSize(size);
},
};

await this.addNotification();
this.postMessage('PROGRESS', { steps: this.steps });

let prevStepId = '';
let prevStepResult: unknown = undefined;
for (let stepIdx = 0; stepIdx < this.steps.length; stepIdx++) {
const step = this.steps[stepIdx];
this.setStatus(stepIdx, 'active', {});
this.postMessage('PROGRESS', { steps: this.steps });
const stepInfo = this.stepsInput.get(step.name)!;
this.setStatus(stepIdx, 'active', {
expiresAt: stepInfo.maxDuration
? new Date(Date.now() + stepInfo.maxDuration).valueOf()
: undefined,
});
try {
prevStepResult = await this.stepsInput
.get(step.name)!
Expand Down
9 changes: 8 additions & 1 deletion src/content/keyAutoAdd/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { ErrorResponse } from '@/shared/messages';

export interface StepRunParams extends BeginPayload {
skip: (message: string | Error | ErrorWithKeyLike) => never;
setNotificationSize: (size: 'notification' | 'fullscreen') => void;
}

export type StepRun<T = unknown, R = void> = (
Expand All @@ -15,6 +16,7 @@ export type StepRun<T = unknown, R = void> = (
export interface Step<T = unknown, R = unknown> {
name: string;
run: StepRun<T, R>;
maxDuration?: number;
}

export type Details = Omit<ErrorResponse, 'success'>;
Expand All @@ -24,7 +26,11 @@ interface StepWithStatusBase {
status: string;
}
interface StepWithStatusNormal extends StepWithStatusBase {
status: 'pending' | 'active' | 'success';
status: 'pending' | 'success';
}
interface StepWithStatusActive extends StepWithStatusBase {
status: 'active';
expiresAt?: number;
}
interface StepWithStatusSkipped extends StepWithStatusBase {
status: 'skipped';
Expand All @@ -37,6 +43,7 @@ interface StepWithStatusError extends StepWithStatusBase {

export type StepWithStatus =
| StepWithStatusNormal
| StepWithStatusActive
| StepWithStatusSkipped
| StepWithStatusError;

Expand Down
21 changes: 17 additions & 4 deletions src/content/keyAutoAdd/testWallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,19 @@ type AccountDetails = {
};
};

const waitForLogin: Step<never> = async ({ skip, keyAddUrl }) => {
let alreadyLoggedIn = false;
const waitForLogin: Step<never> = async ({
skip,
setNotificationSize,
keyAddUrl,
}) => {
let alreadyLoggedIn = window.location.href.startsWith(keyAddUrl);
if (!alreadyLoggedIn) setNotificationSize('notification');
try {
alreadyLoggedIn = await waitForURL(
(url) => (url.origin + url.pathname).startsWith(keyAddUrl),
{ timeout: LOGIN_WAIT_TIMEOUT },
);
setNotificationSize('fullscreen');
} catch (error) {
if (isTimedOut(error)) {
throw new ErrorWithKey('connectWalletKeyService_error_timeoutLogin');
Expand All @@ -45,7 +51,10 @@ const waitForLogin: Step<never> = async ({ skip, keyAddUrl }) => {
}
};

const getAccountDetails: Step<never, Account[]> = async (_) => {
const getAccountDetails: Step<never, Account[]> = async ({
setNotificationSize,
}) => {
setNotificationSize('fullscreen');
await sleep(1000);

const NEXT_DATA = document.querySelector('script#__NEXT_DATA__')?.textContent;
Expand Down Expand Up @@ -155,7 +164,11 @@ async function revokeKey(accountId: string, walletId: string, keyId: string) {

// region: Main
new KeyAutoAdd([
{ name: 'Waiting for login', run: waitForLogin },
{
name: 'Waiting for you to login',
run: waitForLogin,
maxDuration: LOGIN_WAIT_TIMEOUT,
},
{ name: 'Getting account details', run: getAccountDetails },
{ name: 'Revoking existing key', run: revokeExistingKey },
{ name: 'Finding wallet', run: findWallet },
Expand Down
2 changes: 1 addition & 1 deletion src/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
},
"web_accessible_resources": [
{
"resources": ["polyfill/*"],
"resources": ["polyfill/*", "pages/progress-connect/*"],
raducristianpopa marked this conversation as resolved.
Show resolved Hide resolved
"matches": ["<all_urls>"]
}
],
Expand Down
21 changes: 21 additions & 0 deletions src/pages/progress-connect/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import React from 'react';
import { useUIMode } from '@/pages/progress-connect/context';
import { AppNotification } from '@/pages/progress-connect/components/AppNotification';
import { AppFullscreen } from '@/pages/progress-connect/components/AppFullScreen';

export default function App() {
const mode = useUIMode();

React.useEffect(() => {
const container = document.getElementById('container')!;
container.style.height = mode === 'fullscreen' ? '100vh' : 'auto';
document.body.style.backgroundColor =
mode === 'fullscreen' ? 'rgba(255, 255, 255, 0.95)' : 'white';
}, [mode]);

if (mode === 'fullscreen') {
return <AppFullscreen />;
} else {
return <AppNotification />;
}
}
25 changes: 25 additions & 0 deletions src/pages/progress-connect/components/AppFullScreen.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import React from 'react';
import { useState } from '@/pages/progress-connect/context';
import { useBrowser } from '@/popup/lib/context';
import { Steps } from './Steps';

export function AppFullscreen() {
const browser = useBrowser();
const state = useState();

const Logo = browser.runtime.getURL('assets/images/logo.svg');

return (
<div className="m-auto flex h-full w-full max-w-80 flex-col items-center justify-center space-y-2 p-4 text-center">
<header>
<img src={Logo} alt="" className="mx-auto mb-2 h-20" />
<h1 className="text-2xl text-strong">Web Monetization</h1>
<h2 className="text-lg text-weak">Connecting wallet…</h2>
</header>
<main className="w-full space-y-2 pt-2">
<Steps steps={state.steps} />
<p className="text-xs text-weak">{state.currentStep.name}…</p>
</main>
</div>
);
}
Loading