Skip to content
Closed
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
18 changes: 18 additions & 0 deletions .github/actions/setup/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
name: Setup
runs:
using: composite
steps:
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
registry-url: https://registry.npmjs.org
cache: yarn

# For provenance https://docs.npmjs.com/generating-provenance-statements#prerequisites
- name: Install npm 9.5
run: npm install -g npm@^9.5.0
shell: bash

- name: Install node modules
run: yarn install --frozen-lockfile
shell: bash
42 changes: 42 additions & 0 deletions .github/workflows/release-canary.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
name: Release (Canary)
on:
workflow_dispatch:

jobs:
canary:
name: Release canary
runs-on: ubuntu-latest
environment: release
permissions:
contents: write
pull-requests: write
id-token: write
timeout-minutes: 5
steps:
- name: Checkout
uses: actions/checkout@v4
with:
submodules: recursive

- name: 'Setup'
uses: ./.github/actions/setup

- name: Set version
run: npm --no-git-tag-version version $(node -p "require('./packages/wallet-sdk/package.json').version")-canary.$(date +'%Y%m%d') -w packages/wallet-sdk

# Build the package
- name: Prebuild
run: yarn workspace @coinbase/wallet-sdk prebuild

- name: Build Packages
shell: bash
run: yarn build:packages

- name: Set deployment token
run: npm config set '//registry.npmjs.org/:_authToken' "${{ secrets.NPM_TOKEN }}"

- name: Publish to npm
run: cd packages/wallet-sdk && npm publish --tag canary
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

69 changes: 69 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
name: Release

on:
workflow_dispatch:
inputs:
packageVersion:
description: "The version to publish in MAJOR.MINOR.PATCH format"
required: true
default: ""

jobs:
authorize:
name: Authorize
runs-on: ubuntu-latest
steps:
- name: ${{ github.actor }} permission check to update release version
uses: "lannonbr/[email protected]"
with:
permission: "write"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

release:

Check warning

Code scanning / CodeQL

Workflow does not contain permissions Medium

Actions job or workflow does not limit the permissions of the GITHUB_TOKEN. Consider setting an explicit permissions block, using the following as a minimal starting point: {}
name: Release
runs-on: ubuntu-latest
environment: release
permissions:
contents: write
pull-requests: write
id-token: write
steps:
- name: Checkout
uses: actions/checkout@v4
with:
submodules: recursive

- name: 'Setup'
uses: ./.github/actions/setup

- name: Set version
run: npm version ${{ env.PACKAGE_VERSION }} -w packages/wallet-sdk

# Build the package
- name: Prebuild
run: yarn workspace @coinbase/wallet-sdk prebuild

- name: Build SDK
shell: bash
run: yarn workspace @coinbase/wallet-sdk build

# Create a pull request to update the version
- name: Create pull request
uses: peter-evans/create-pull-request@v4
with:
title: "[Version update] v${{ env.PACKAGE_VERSION }}"
body: "Automated workflow: version update"
branch: release-v${{ env.PACKAGE_VERSION }}
delete-branch: true
commit-message: "v${{ env.PACKAGE_VERSION }}"
labels: version-update

# Publish to npm
- name: Set deployment token
run: npm config set '//registry.npmjs.org/:_authToken' "${{ secrets.NPM_TOKEN }}"

- name: Publish to npm
run: cd packages/wallet-sdk && npm publish --tag latest
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
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.2",
"description": "Coinbase Wallet JavaScript SDK",
"keywords": [
"coinbase",
Expand Down
2 changes: 1 addition & 1 deletion packages/wallet-sdk/src/core/communicator/Communicator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion packages/wallet-sdk/src/core/constants.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export const CB_KEYS_URL = 'https://keys.coinbase.com/connect';
export const CB_WALLET_RPC_URL = 'http://rpc.wallet.coinbase.com';
export const CB_WALLET_RPC_URL = 'https://rpc.wallet.coinbase.com';
export const WALLETLINK_URL = 'https://www.walletlink.org';
export const CBW_MOBILE_DEEPLINK_URL = 'https://go.cb-w.com/walletlink';
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand Down
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', () => {
Expand All @@ -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,
Expand All @@ -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', () => {
Expand Down
78 changes: 67 additions & 11 deletions packages/wallet-sdk/src/util/web.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,76 @@
import { NAME, VERSION } from '../sdk-info.js';
import { getCrossOriginOpenerPolicy } from './checkCrossOriginOpenerPolicy.js';
import { standardErrors } from ':core/error/errors.js';
import { Snackbar } from ':sign/walletlink/relay/ui/components/Snackbar/Snackbar.js';
import { RETRY_SVG_PATH } from ':sign/walletlink/relay/ui/WalletLinkRelayUI.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) {
Expand All @@ -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
Expand Up @@ -13,7 +13,8 @@
"outDir": "./dist",
"paths": {
":util/*": ["src/util/*"],
":core/*": ["src/core/*"]
":core/*": ["src/core/*"],
":sign/*": ["src/sign/*"]
},
"target": "es2017",
"jsx": "react",
Expand Down
Loading