Skip to content
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
7 changes: 6 additions & 1 deletion api-specs/openrpc-user-api.json
Original file line number Diff line number Diff line change
Expand Up @@ -138,9 +138,14 @@
"title": "clientId",
"type": "string",
"description": "Client ID used as the JWT subject"
},
"clientSecret": {
"title": "clientSecret",
"type": "string",
"description": "Client secret that must match the selected network's auth.clientSecret"
}
},
"required": ["networkId", "clientId"]
"required": ["networkId", "clientId", "clientSecret"]
}
}
],
Expand Down
38 changes: 37 additions & 1 deletion core/wallet-test-utils/src/wallet-gateway.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@ export type WalletGatewayArgs =
page: Page
}

export type LoginCredentials = {
clientId?: string
clientSecret?: string
}

export class WalletGateway {
private readonly isPopup: boolean
private readonly dappPage: Page | undefined
Expand Down Expand Up @@ -105,6 +110,7 @@ export class WalletGateway {
async connect(args: {
network: string
customURL?: string
credentials?: LoginCredentials
}): Promise<void> {
await test.step(`wallet gateway: connect to ${args.network}`, async () => {
const dapp = this.requireDapp()
Expand All @@ -127,6 +133,7 @@ export class WalletGateway {
'the wallet gateway has a network select'
).toBeVisible()
await selectNetwork.selectOption({ label: args.network })
await this.fillLoginCredentials(popup, args.credentials)
const confirmConnectButton = popup.getByRole('button', {
name: 'Connect',
})
Expand Down Expand Up @@ -533,8 +540,36 @@ export class WalletGateway {
throw new Error('wallet connect form popup did not appear')
}

private async fillLoginCredentials(
page: Page,
credentials?: LoginCredentials
): Promise<void> {
const clientId = credentials?.clientId
if (clientId !== undefined) {
const clientIdInput = page.getByLabel('Client ID')
await expect(
clientIdInput,
'Client ID was provided but the login form has no Client ID input'
).toBeVisible()
await clientIdInput.fill(clientId)
}

const clientSecret = credentials?.clientSecret
if (clientSecret !== undefined) {
const clientSecretInput = page.getByLabel('Client Secret')
await expect(
clientSecretInput,
'Client Secret was provided but the login form has no Client Secret input'
).toBeVisible()
await clientSecretInput.fill(clientSecret)
}
}

// Logs in to a gateway that was opened directly, without a dApp.
async login(network: string): Promise<void> {
async login(
network: string,
credentials?: LoginCredentials
): Promise<void> {
await test.step(`wallet gateway: log in to ${network}`, async () => {
const page = await this.page()
const selectNetwork = page.getByLabel('Select a network')
Expand All @@ -543,6 +578,7 @@ export class WalletGateway {
'the wallet gateway has a network select'
).toBeVisible()
await selectNetwork.selectOption({ label: network })
await this.fillLoginCredentials(page, credentials)
await page.getByRole('button', { name: 'Connect' }).click()
// Wait for the OAuth redirect chain to complete and land on the parties page.
await page.waitForURL(/\/parties/, { timeout: 30000 })
Expand Down
56 changes: 56 additions & 0 deletions core/wallet-ui-components/src/components/login-form.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,62 @@ describe('wg-login-form', () => {
const event = listener.mock.calls[0][0] as LoginConnectEvent
expect(event.selectedNetwork).toBe(network)
expect(event.selectedIdp).toBe(idp)
expect(event.clientId).toBeUndefined()
expect(event.clientSecret).toBeUndefined()
})

it('emits the entered client secret for self-signed identity providers', async () => {
const network = makePublicNetwork({
identityProviderId: 'idp-1',
clientId: 'client-id',
})
const idp = makeIdp({ id: 'idp-1', type: 'self_signed' })

const el = await fixture<WgLoginForm>(
html`<wg-login-form
.networks=${[network]}
.idps=${[idp]}
></wg-login-form>`
)

const secretInput =
el.shadowRoot!.querySelector<HTMLInputElement>('#client-secret')!
expect(secretInput.type).toBe('password')
secretInput.value = 'network-secret'

const listener = vi.fn()
el.addEventListener('login-connect', listener)

el.shadowRoot!.querySelector<HTMLButtonElement>('.connect-btn')!.click()

expect(listener).toHaveBeenCalledOnce()
const event = listener.mock.calls[0][0] as LoginConnectEvent
expect(event.clientId).toBe('client-id')
expect(event.clientSecret).toBe('network-secret')
})

it('submits on form submit event', async () => {
const network = makePublicNetwork({
identityProviderId: 'idp-1',
clientId: 'client-id',
})
const idp = makeIdp({ id: 'idp-1' })

const el = await fixture<WgLoginForm>(
html`<wg-login-form
.networks=${[network]}
.idps=${[idp]}
></wg-login-form>`
)

const listener = vi.fn()
el.addEventListener('login-connect', listener)

el.shadowRoot!.querySelector('form')!.dispatchEvent(
new Event('submit', { bubbles: true, cancelable: true })
)

expect(listener).toHaveBeenCalledOnce()
expect(listener.mock.calls[0][0]).toBeInstanceOf(LoginConnectEvent)
})
})
64 changes: 50 additions & 14 deletions core/wallet-ui-components/src/components/login-form.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ export class LoginConnectEvent extends Event {
constructor(
public selectedNetwork: PublicNetwork,
public selectedIdp: Idp,
public clientId: string
public clientId?: string,
public clientSecret?: string
) {
super('login-connect', { bubbles: true, composed: true })
}
Expand Down Expand Up @@ -99,7 +100,7 @@ export class WgLoginForm extends BaseElement {
}

.network-select,
.client-id-input {
.login-input {
width: 100%;
border: 1px solid #d4d4d8;
border-radius: 4px;
Expand All @@ -112,7 +113,7 @@ export class WgLoginForm extends BaseElement {
}

.network-select:focus,
.client-id-input:focus {
.login-input:focus {
border-color: var(--wg-input-border-focus);
box-shadow: 0 0 0 3px rgba(var(--wg-accent-rgb), 0.12);
}
Expand Down Expand Up @@ -215,18 +216,40 @@ export class WgLoginForm extends BaseElement {
return
}

const clientId =
(
this.renderRoot.querySelector(
'#client-id'
) as HTMLInputElement | null
)?.value || this.selectedNetwork.clientId
let clientId: string | undefined
let clientSecret: string | undefined

if (idp.type === 'self_signed') {
clientId =
(
this.renderRoot.querySelector(
'#client-id'
) as HTMLInputElement | null
)?.value || this.selectedNetwork.clientId

clientSecret =
(
this.renderRoot.querySelector(
'#client-secret'
) as HTMLInputElement | null
)?.value ?? ''
}

this.dispatchEvent(
new LoginConnectEvent(this.selectedNetwork, idp, clientId || '')
new LoginConnectEvent(
this.selectedNetwork,
idp,
clientId,
clientSecret
)
)
}

private handleSubmit(e: Event) {
e.preventDefault()
this.handleConnect()
}

/** Set a status message on the form (e.g. "Redirecting...") */
setMessage(message: string, type: 'error' | 'info') {
this.message = message
Expand All @@ -241,7 +264,7 @@ export class WgLoginForm extends BaseElement {

protected render() {
return html`
<main class="screen">
<form class="screen" @submit=${this.handleSubmit}>
<div class="top-bar">
<img class="top-logo" src=${cantonLogo} alt="Canton logo" />
</div>
Expand Down Expand Up @@ -293,11 +316,24 @@ export class WgLoginForm extends BaseElement {
>
<input
id="client-id"
class="client-id-input form-control"
class="login-input form-control"
type="text"
autocomplete="username"
.value=${this.selectedNetwork?.clientId || ''}
?disabled=${this.connecting}
/>
<label
class="form-label fw-semibold text-body mt-3 mb-2"
for="client-secret"
>Client Secret</label
>
<input
id="client-secret"
class="login-input form-control"
type="password"
autocomplete="current-password"
?disabled=${this.connecting}
/>
`
: null
}
Expand Down Expand Up @@ -329,8 +365,8 @@ export class WgLoginForm extends BaseElement {

<div class="footer">
<button
type="submit"
class="connect-btn btn btn-primary w-100 rounded-pill"
@click=${this.handleConnect}
?disabled=${
this.loading ||
this.connecting ||
Expand All @@ -340,7 +376,7 @@ export class WgLoginForm extends BaseElement {
${this.connecting ? 'Connecting…' : 'Connect'}
</button>
</div>
</main>
</form>
`
}
}
1 change: 1 addition & 0 deletions core/wallet-user-rpc-client/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -511,6 +511,7 @@ export interface GetNetworkParams {
export interface SelfSignedAccessTokenParams {
networkId: NetworkId
clientId: ClientId
clientSecret: ClientSecret
}
export interface AddIdpParams {
idp: Idp
Expand Down
7 changes: 6 additions & 1 deletion core/wallet-user-rpc-client/src/openrpc.json
Original file line number Diff line number Diff line change
Expand Up @@ -138,9 +138,14 @@
"title": "clientId",
"type": "string",
"description": "Client ID used as the JWT subject"
},
"clientSecret": {
"title": "clientSecret",
"type": "string",
"description": "Client secret that must match the selected network's auth.clientSecret"
}
},
"required": ["networkId", "clientId"]
"required": ["networkId", "clientId", "clientSecret"]
}
}
],
Expand Down
3 changes: 2 additions & 1 deletion examples/portfolio/tests/allocation.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { expect, type Page, test } from '@playwright/test'
import { OTCTrade } from '@canton-network/core-wallet-test-utils'
import {
createWalletGateway,
connectToLocalNet,
gotoConnect,
setupRegistry,
switchWallet,
Expand Down Expand Up @@ -69,7 +70,7 @@ const setupOtcTrade = async (page: Page) => {
const wg = createWalletGateway(page)

await gotoConnect(page)
await wg.connect({ network: 'LocalNet' })
await connectToLocalNet(wg)

const venueHint = `venue-${rnd}`
const aliceHint = `alice-${rnd}`
Expand Down
5 changes: 3 additions & 2 deletions examples/portfolio/tests/preapprovals.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import { test } from '@playwright/test'
import {
createWalletGateway,
connectToLocalNet,
expectTransferOfferGone,
expectWalletBalance,
fillAndSubmitTransfer,
Expand Down Expand Up @@ -40,7 +41,7 @@ test('toggle preapproval', async ({ page: dappPage }) => {
const wg = createWalletGateway(dappPage)

await gotoConnect(dappPage)
await wg.connect({ network: 'LocalNet' })
await connectToLocalNet(wg)

const alice = await wg.createWalletIfNotExists({
partyHint: `alice-${rnd}`,
Expand Down Expand Up @@ -69,7 +70,7 @@ test('one step transfer to preapproved receiver', async ({
const wg = createWalletGateway(dappPage)

await gotoConnect(dappPage)
await wg.connect({ network: 'LocalNet' })
await connectToLocalNet(wg)

const alice = await wg.createWalletIfNotExists({
partyHint: `alice-${rnd}`,
Expand Down
5 changes: 3 additions & 2 deletions examples/portfolio/tests/settings.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { toPortfolioInstrument } from '../src/types/instruments'
import { normalizeRegistryUrl } from '../src/utils/registry'
import {
createWalletGateway,
connectToLocalNet,
expectWalletBalance,
gotoConnect,
setupRegistry,
Expand All @@ -24,7 +25,7 @@ const connectToSettings = async (page: Page) => {
const wg = createWalletGateway(page)

await gotoConnect(page)
await wg.connect({ network: 'LocalNet' })
await connectToLocalNet(wg)
await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible({
timeout: 15000,
})
Expand Down Expand Up @@ -250,7 +251,7 @@ test('tap via settings page', async ({ page: dappPage }) => {
const wg = createWalletGateway(dappPage)

await gotoConnect(dappPage)
await wg.connect({ network: 'LocalNet' })
await connectToLocalNet(wg)

const alice = await wg.createWalletIfNotExists({
partyHint: `alice-${rnd}`,
Expand Down
3 changes: 2 additions & 1 deletion examples/portfolio/tests/transaction-history.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { expect, type Locator, type Page, test } from '@playwright/test'
import { fundValidatorOperator } from './fund-validator'
import {
createWalletGateway,
connectToLocalNet,
fillAndSubmitTransfer,
gotoConnect,
gotoDashboard,
Expand Down Expand Up @@ -131,7 +132,7 @@ test('shows taps, direct transfers, and transfer offers for both parties', async
const wg = createWalletGateway(dappPage)

await gotoConnect(dappPage)
await wg.connect({ network: 'LocalNet' })
await connectToLocalNet(wg)

const alice = await wg.createWalletIfNotExists({
partyHint: aliceHint,
Expand Down
Loading
Loading