Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: coinbase/coinbase-wallet-sdk
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: v4.3.0
Choose a base ref
...
head repository: coinbase/coinbase-wallet-sdk
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: 8ce5316093899d2e39e5fb8ed10c242949493958
Choose a head ref
  • 4 commits
  • 8 files changed
  • 1 contributor

Commits on Mar 13, 2025

  1. Add blocked popup detection with UI to retry (#1538)

    * Add blocked popup detection with UI to retry
    
    * Remove test code
    
    * Share retry path
    
    * typecheck
    arjun-dureja committed Mar 13, 2025

    Verified

    This commit was signed with the committer’s verified signature.
    joshka Josh McKinney
    Copy the full SHA
    3dde198 View commit details
  2. Bump version

    arjun-dureja committed Mar 13, 2025

    Verified

    This commit was signed with the committer’s verified signature.
    Copy the full SHA
    700aac5 View commit details

Commits on Mar 17, 2025

  1. fix imports

    arjun-dureja committed Mar 17, 2025

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    1f5fcb6 View commit details
  2. update imports

    arjun-dureja committed Mar 17, 2025

    Verified

    This commit was signed with the committer’s verified signature.
    joshka Josh McKinney
    Copy the full SHA
    8ce5316 View commit details
2 changes: 1 addition & 1 deletion packages/wallet-sdk/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@coinbase/wallet-sdk",
"version": "4.3.0",
"version": "4.3.1",
"description": "Coinbase Wallet JavaScript SDK",
"keywords": [
"coinbase",
2 changes: 1 addition & 1 deletion packages/wallet-sdk/src/core/communicator/Communicator.ts
Original file line number Diff line number Diff line change
@@ -99,7 +99,7 @@ export class Communicator {
return this.popup;
}

this.popup = openPopup(this.url);
this.popup = await openPopup(this.url);

this.onMessage<ConfigMessage>(({ event }) => event === 'PopupUnload')
.then(this.disconnect)
2 changes: 1 addition & 1 deletion packages/wallet-sdk/src/sdk-info.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export const VERSION = '4.3.0';
export const VERSION = '4.3.1';
export const NAME = '@coinbase/wallet-sdk';
Original file line number Diff line number Diff line change
@@ -2,6 +2,9 @@ import { injectCssReset } from './components/cssReset/cssReset.js';
import { Snackbar, SnackbarInstanceProps } from './components/Snackbar/Snackbar.js';
import { RelayUI } from './RelayUI.js';

export const RETRY_SVG_PATH =
'M5.00008 0.96875C6.73133 0.96875 8.23758 1.94375 9.00008 3.375L10.0001 2.375V5.5H9.53133H7.96883H6.87508L7.80633 4.56875C7.41258 3.3875 6.31258 2.53125 5.00008 2.53125C3.76258 2.53125 2.70633 3.2875 2.25633 4.36875L0.812576 3.76875C1.50008 2.125 3.11258 0.96875 5.00008 0.96875ZM2.19375 6.43125C2.5875 7.6125 3.6875 8.46875 5 8.46875C6.2375 8.46875 7.29375 7.7125 7.74375 6.63125L9.1875 7.23125C8.5 8.875 6.8875 10.0312 5 10.0312C3.26875 10.0312 1.7625 9.05625 1 7.625L0 8.625V5.5H0.46875H2.03125H3.125L2.19375 6.43125Z';

export class WalletLinkRelayUI implements RelayUI {
private readonly snackbar: Snackbar;
private attached = false;
@@ -67,7 +70,7 @@ export class WalletLinkRelayUI implements RelayUI {
info: 'Reset connection',
svgWidth: '10',
svgHeight: '11',
path: 'M5.00008 0.96875C6.73133 0.96875 8.23758 1.94375 9.00008 3.375L10.0001 2.375V5.5H9.53133H7.96883H6.87508L7.80633 4.56875C7.41258 3.3875 6.31258 2.53125 5.00008 2.53125C3.76258 2.53125 2.70633 3.2875 2.25633 4.36875L0.812576 3.76875C1.50008 2.125 3.11258 0.96875 5.00008 0.96875ZM2.19375 6.43125C2.5875 7.6125 3.6875 8.46875 5 8.46875C6.2375 8.46875 7.29375 7.7125 7.74375 6.63125L9.1875 7.23125C8.5 8.875 6.8875 10.0312 5 10.0312C3.26875 10.0312 1.7625 9.05625 1 7.625L0 8.625V5.5H0.46875H2.03125H3.125L2.19375 6.43125Z',
path: RETRY_SVG_PATH,
defaultFillRule: 'evenodd',
defaultClipRule: 'evenodd',
onClick: options.onResetConnection,
67 changes: 60 additions & 7 deletions packages/wallet-sdk/src/util/web.test.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,27 @@
import { waitFor } from '@testing-library/preact';
import { Mock, vi } from 'vitest';

import { NAME, VERSION } from '../sdk-info.js';
import { getCrossOriginOpenerPolicy } from './checkCrossOriginOpenerPolicy.js';
import { closePopup, openPopup } from './web.js';
import { standardErrors } from ':core/error/errors.js';

vi.mock('./checkCrossOriginOpenerPolicy');
(getCrossOriginOpenerPolicy as Mock).mockReturnValue('null');

// Mock Snackbar class
const mockPresentItem = vi.fn().mockReturnValue(() => {});
const mockClear = vi.fn();
const mockAttach = vi.fn();
const mockInstance = {
presentItem: mockPresentItem,
clear: mockClear,
attach: mockAttach,
};

vi.mock(':sign/walletlink/relay/ui/components/Snackbar/Snackbar.js', () => ({
Snackbar: vi.fn().mockImplementation(() => mockInstance),
}));

const mockOrigin = 'http://localhost';

describe('PopupManager', () => {
@@ -28,11 +42,11 @@ describe('PopupManager', () => {
vi.clearAllMocks();
});

it('should open a popup with correct settings and focus it', () => {
it('should open a popup with correct settings and focus it', async () => {
const url = new URL('https://example.com');
(window.open as Mock).mockReturnValue({ focus: vi.fn() });

const popup = openPopup(url);
const popup = await openPopup(url);

expect(window.open).toHaveBeenNthCalledWith(
1,
@@ -48,12 +62,51 @@ describe('PopupManager', () => {
expect(url.searchParams.get('coop')).toBe('null');
});

it('should throw an error if popup fails to open', () => {
it('should show snackbar with retry button when popup is blocked and retry successfully', async () => {
const url = new URL('https://example.com');
const mockPopup = { focus: vi.fn() };
(window.open as Mock).mockReturnValueOnce(null).mockReturnValueOnce(mockPopup);

const promise = openPopup(url);

await waitFor(() => {
expect(mockPresentItem).toHaveBeenCalledWith(
expect.objectContaining({
autoExpand: true,
message: 'Popup was blocked. Try again.',
})
);
});

const retryButton = mockPresentItem.mock.calls[0][0].menuItems[0];
retryButton.onClick();

const popup = await promise;
expect(popup).toBe(mockPopup);
expect(mockClear).toHaveBeenCalled();
expect(window.open).toHaveBeenCalledTimes(2);
});

it('should show snackbar with retry button when popup is blocked and reject if retry fails', async () => {
const url = new URL('https://example.com');
(window.open as Mock).mockReturnValue(null);

expect(() => openPopup(new URL('https://example.com'))).toThrow(
standardErrors.rpc.internal('Pop up window failed to open')
);
const promise = openPopup(url);

await waitFor(() => {
expect(mockPresentItem).toHaveBeenCalledWith(
expect.objectContaining({
autoExpand: true,
message: 'Popup was blocked. Try again.',
})
);
});

const retryButton = mockPresentItem.mock.calls[0][0].menuItems[0];
retryButton.onClick();

await expect(promise).rejects.toThrow('Popup window was blocked');
expect(mockClear).toHaveBeenCalled();
});

it('should close an open popup window', () => {
80 changes: 68 additions & 12 deletions packages/wallet-sdk/src/util/web.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,76 @@
import { standardErrors } from '../core/error/errors.js';
import { NAME, VERSION } from '../sdk-info.js';
import { Snackbar } from ':sign/walletlink/relay/ui/components/Snackbar/Snackbar.js';
import { RETRY_SVG_PATH } from ':sign/walletlink/relay/ui/WalletLinkRelayUI.js';
import { getCrossOriginOpenerPolicy } from './checkCrossOriginOpenerPolicy.js';
import { standardErrors } from ':core/error/errors.js';

const POPUP_WIDTH = 420;
const POPUP_HEIGHT = 540;

// Window Management
const RETRY_BUTTON = {
isRed: false,
info: 'Retry',
svgWidth: '10',
svgHeight: '11',
path: RETRY_SVG_PATH,
defaultFillRule: 'evenodd',
defaultClipRule: 'evenodd',
} as const;

export function openPopup(url: URL): Window {
const POPUP_BLOCKED_MESSAGE = 'Popup was blocked. Try again.';

let snackbar: Snackbar | null = null;

export function openPopup(url: URL): Promise<Window> {
const left = (window.innerWidth - POPUP_WIDTH) / 2 + window.screenX;
const top = (window.innerHeight - POPUP_HEIGHT) / 2 + window.screenY;
appendAppInfoQueryParams(url);

const popupId = `wallet_${crypto.randomUUID()}`;
const popup = window.open(
url,
popupId,
`width=${POPUP_WIDTH}, height=${POPUP_HEIGHT}, left=${left}, top=${top}`
);
function tryOpenPopup(): Window | null {
const popupId = `wallet_${crypto.randomUUID()}`;
const popup = window.open(
url,
popupId,
`width=${POPUP_WIDTH}, height=${POPUP_HEIGHT}, left=${left}, top=${top}`
);

popup?.focus();

popup?.focus();
if (!popup) {
return null;
}

return popup;
}

let popup = tryOpenPopup();

// If the popup was blocked, show a snackbar with a retry button
if (!popup) {
throw standardErrors.rpc.internal('Pop up window failed to open');
const sb = initSnackbar();
return new Promise<Window>((resolve, reject) => {
sb.presentItem({
autoExpand: true,
message: POPUP_BLOCKED_MESSAGE,
menuItems: [
{
...RETRY_BUTTON,
onClick: () => {
popup = tryOpenPopup();
if (popup) {
resolve(popup);
} else {
reject(standardErrors.rpc.internal('Popup window was blocked'));
}
sb.clear();
},
},
],
});
});
}

return popup;
return Promise.resolve(popup);
}

export function closePopup(popup: Window | null) {
@@ -46,3 +91,14 @@ function appendAppInfoQueryParams(url: URL) {
url.searchParams.append(key, value.toString());
}
}

function initSnackbar() {
if (!snackbar) {
const root = document.createElement('div');
root.className = '-cbwsdk-css-reset';
document.body.appendChild(root);
snackbar = new Snackbar();
snackbar.attach(root);
}
return snackbar;
}
3 changes: 2 additions & 1 deletion packages/wallet-sdk/tsconfig.base.json
Original file line number Diff line number Diff line change
@@ -13,7 +13,8 @@
"outDir": "./dist",
"paths": {
":util/*": ["src/util/*"],
":core/*": ["src/core/*"]
":core/*": ["src/core/*"],
":sign/*": ["src/sign/*"]
},
"target": "es2017",
"jsx": "react",
1 change: 1 addition & 0 deletions packages/wallet-sdk/vitest.config.ts
Original file line number Diff line number Diff line change
@@ -6,6 +6,7 @@ export default defineConfig({
alias: {
':core': path.resolve(__dirname, 'src/core'),
':util': path.resolve(__dirname, 'src/util'),
':sign': path.resolve(__dirname, 'src/sign'),
},
environment: 'jsdom',
globals: true,