Skip to content

Add same-device wallet links to the wallet chooser - #200

Open
djscruggs wants to merge 21 commits into
mainfrom
feature/same-device-wallet-link
Open

Add same-device wallet links to the wallet chooser#200
djscruggs wants to merge 21 commits into
mainfrom
feature/same-device-wallet-link

Conversation

@djscruggs

@djscruggs djscruggs commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Phase 2 of cross-device-wallet-selection-spec.md, whose Phase 1 (the cross-device QR code) shipped in 7.2.0. Addresses #166.

What this adds

Two rows in the wallet chooser, shown whenever a relying party supplies an
interaction URL:

  • Open my wallet app — hands the interaction URL to the OS as
    interaction:<url>
  • Open my web wallet — hands it to the browser as
    web+interaction:<url>

They reach a wallet that registered a URL scheme but is not registered as a
credential handler in this browser. Such a wallet was previously unreachable:
it never appears in the chooser, and a QR code cannot be scanned by the device
displaying it — the case a phone always hits.

Why it belongs here and not in coordinator sites

The same argument that moved the QR code into CHAPI. A coordinator site cannot
know which wallets a user has registered, so it cannot decide which link to
show, or whether to show one at all. The chooser can. Coordinators keep a
single button that calls CHAPI and get this behavior — and later fixes —
without changing their code.

Each row appears only where it can work

Wallet app Web wallet
Phone / tablet shown shown where the API exists
Desktop hidden (QR serves it) shown where the API exists
Safari, or any browser on iOS shown hidden
  • Wallet app is gated on a mobile device, detected by pointer capability
    rather than viewport width: the chooser renders in a 500px popup on the
    desktop, the same width range as a phone, so a breakpoint cannot tell them
    apart.
  • Web wallet is gated on navigator.registerProtocolHandler() existing.
    Nothing can claim a web+ scheme without it, and Safari has never shipped
    the API — there the link cannot resolve and the browser reports the address
    as invalid, so the row would offer a guaranteed error. This covers every
    browser on iOS, since they are all WebKit; feature detection gets that right
    without enumerating browsers.

Neither row is a fallback for the other: a native app can only claim the bare
scheme, a web app only the web+ form, and nothing in the browser reveals
which the user registered. Custom-scheme navigation also fails silently, with
no event to detect it, so no fallback can be inferred. The user knows whether
their wallet is an app or a website, so both are offered where both can work
and the copy says what silence means.

Layout

Screenshots from the /test/wallet-chooser harness on a real iPhone, two per
browser: no registered wallet, and one registered wallet. Chrome on iOS is
WebKit under the hood, so it hits the same gates as Safari — in all four only
the wallet app row appears, because neither browser has
navigator.registerProtocolHandler() and so neither can claim
web+interaction:.

Chrome on iOS

No wallet registered One wallet registered

Chrome on iOS with no registered wallet: one-line greeting, the wallet app row highlighted with a blue accent bar, the silent-failure note, and the QR code expanded below

Chrome on iOS with Demo Wallet 1 listed: the wallet list, then the heading and the wallet app row in neutral styling, then the collapsed QR expander

Greeting is one line. The wallet app row is highlighted — with nothing registered, an installed app is the likeliest thing the user has. The QR opens automatically, since it is the only other option. The wallet row keeps precedence, so the link row is neutral and carries the "Already have a wallet?" heading. The QR stays behind its expander.

Safari on iOS

No wallet registered One wallet registered

Safari on iOS with no registered wallet: same layout as Chrome, wallet app row highlighted, QR expanded

Safari on iOS with Demo Wallet 1 listed: same layout as Chrome, neutral wallet app row, collapsed QR expander

Identical to Chrome above — the difference between the pairs is the browser's own chrome, not the dialog. Same. Both engines resolve the gates the same way.

Note the failure note reads "this link", singular, in all four: the copy adapts
when only one row is shown. A build where both rows render — Chrome or Firefox
on Android or the desktop — shows "these links".

The rows sit between the wallet list and the QR expander, in their own
always-visible section, using the same wrm-item row shape as the wallet list
— so opening a wallet you already have reads as the same class of action as
picking a listed one. The QR stays behind its expander: it needs a second
device, and it is tall enough to push everything else off a phone screen.

With no wallet registered in this browser, the wallet app row is marked
recommended — there is no registered wallet to prefer and the QR needs a
second device, so an installed app is the likeliest thing the user has.

Tapping a row emits nothing and changes no mediator state. A user who taps has
not cancelled, and if nothing claims the scheme the dialog must remain as it
was; dismissal stays with the existing Close button, which resolves
OutOfBand as Phase 1 established.

Note on the schemes

interaction: and web+interaction: are a Digital Bazaar convention, not a
standard. Verified against the VCALM Editor's Draft on 2026-08-19: it defines
no interaction URL scheme, treating interaction URLs as plain HTTPS carrying
iuv=1. The names live in two constants in web/mediator/constants.js so a
rename after CCG/VCALM agreement is a one-line change.

This ships experimentally, which the parent spec's Open Question 1 permits
("after agreement or experimentally to gather feedback"). Feedback is what
agreement needs.

Also included

  • +20px on the cross-device popup height. The dialog does not size itself to
    its content, so each section added to it is paid for by hand here; without
    it the Close button can scroll out of reach. A stopgap until the dynamic
    sizing work lands.
  • The zero-wallet greeting is cut to one line and points at the QR when no
    link rows are available.
  • The cross-device expander is relabeled "Use a wallet on another device" — it
    read "Don't see your wallet?", which now sits below a section answering that
    question.
  • /test/wallet-chooser gains app and web params to force either row on
    or off, since the real gates depend on the browser.

Testing

npm run test:e2e on chromium, webkit, iphone, and android-pixel: 152 passed.
The 20 failures are the pre-existing has no horizontal overflow assertion,
marked KNOWN FAILING in the spec file and unchanged by this branch (verified by
stashing). Firefox is excluded — it stalls on the author's machine
independently of these changes, including browserContext.newPage timing out
on main.

Verified by hand on a real iPhone against a tunnelled mediator: the wallet app
row appears, the web wallet row does not, and the rows render correctly in the
first-party popup.

Not covered

No web wallet registers web+interaction: yet, so that row is inert where it
shows — Chrome logs "the scheme does not have a registered handler". It is
harmless but unexercised; worth deciding whether to ship it now or hold it
until a wallet registers.

A wallet can register a URL scheme without registering as a credential
handler in this browser, and such a wallet never appears in the chooser.
The interaction URL is already validated for the cross-device QR code, so
prefixing it with a scheme is all that is needed to hand the same exchange
to a wallet on this device.

Two schemes, because a native app can only claim the bare `interaction:`
scheme while a web app must use the `web+interaction:` form that
`registerProtocolHandler()` requires. Nothing in the browser reveals which
the user registered, so both URLs are built and the UI offers both.

Straight concatenation with no percent-encoding, matching the rule already
implemented for the same link elsewhere. Note this is a Digital Bazaar
convention rather than a standard: no normative definition of an
interaction URL scheme exists yet, so the names live in constants.

Returns both URLs from one call so they share a single validation path and
a caller cannot render one while silently omitting the other.

Addresses #166.
The QR code serves the two-device case: it is displayed here and scanned
from a phone. On a phone there is no second device, and a wallet that
registered a URL scheme without registering as a credential handler never
appears in the chooser, so neither existing path reaches it.

Two links, because a native app can only claim `interaction:` while a web
app must use the `web+interaction:` form that `registerProtocolHandler()`
requires, and nothing in the browser reveals which the user registered.
Custom-scheme navigation also fails silently everywhere, with no event to
detect it, so no fallback can be inferred -- the user knows whether their
wallet is an app or a website, which is the one fact neither the page nor
the browser can supply, so both links are offered and the copy says what
silence means rather than promising an error that cannot fire.

Placed above the QR code inside the existing expander: it needs no second
device, so it is the cheaper option when it works. Real anchors rather
than scripted navigation, for keyboard and context-menu behavior.

Tapping a link emits nothing and changes no mediator state. A user who
taps has not cancelled, and if nothing claims the scheme the dialog must
remain exactly as it was; dismissal stays with the existing Close button.

The visibility condition is the interaction URL that already gates the QR,
so a request without one is unchanged.

Addresses #166.
The harness drives the presentational wizard directly, so it has to build
the prefixed URLs the mediator would normally derive. Adds a `link` query
param to exercise the section with the links suppressed and the QR code
still present, which no other combination produces.

The fake interaction URL now carries `iuv=1`, matching what the mediator
requires of a real one. The harness bypasses that validation, so the old
value passed, but it misrepresented the input the layout is built for.

Addresses #166.
Nothing in the browser reveals whether a wallet app is installed, so the
link cannot be shown conditionally on that. Form factor is a property the
browser does report, and it is the one that matters: a wallet app can only
be installed on a phone or tablet, and a desktop is already served by the
QR code, which needs no installed app and no scheme registration.

Detected by pointer capability rather than viewport width. The chooser
renders in a 500px popup on the desktop -- the same width range as a phone
screen -- so a width breakpoint cannot tell them apart. A coarse primary
pointer that cannot hover separates a touch device from a mouse-driven
desktop without parsing user agent strings.

The web wallet link stays on every device, since a registered web wallet
works the same on either, so the desktop chooser now offers one link and
the QR code, and a phone offers both links.

Relabels the second link "Web wallet" and the heading "Already have a
wallet? Open it here:". With the app link absent on the desktop, the pair
reads as a choice between a website and an installed app rather than two
spellings of the same thing. The silent-failure line now points at the QR
code as the fallback.

The harness reads the emulated pointer the same way, and takes an `app`
param to force the link on for layout work on a desktop project.

Addresses #166.
The links were inside the collapsible block with the QR code, so a wallet
the user already has installed was reachable only after expanding "Don't
see your wallet?" -- while registered web wallets were listed immediately.
That ranked an installed wallet below a wallet the user may not have, and
framed it as a fallback for when the listed options fail.

The links now sit in their own always-visible section between the wallet
list and the QR expander, so both ways of reaching a wallet the user
already has are offered at the same level.

The QR code stays behind its expander: it needs a second device, which is
the less common case, and it is tall enough to push everything else off a
phone screen. Its toggle is relabeled "Use a wallet on another device",
since "Don't see your wallet?" now sits below a section that answers that
question and would read as a contradiction.

The zero-wallet greeting no longer says to scan the QR code below, which
was the only option when it was written.

The silent-failure note is de-emphasized: it only matters after a tap that
did nothing, so it should not compete with the links it explains.

Addresses #166.
As a pair of small centered buttons the links read as an afterthought
trailing the wallet list, even after they were moved out from behind the
QR expander. A wallet the user already has should look like the same kind
of choice as a registered one, not like a secondary control.

They now use the same `wrm-item` row shape as the hint list: full width,
icon on the left, a bold title and a caption saying what each one is.
Titles name the action ("Open my wallet app") rather than the object, and
the captions distinguish an installed app from a wallet website -- the
question the user can actually answer, without naming URL schemes.

The rows inherit the dialog's text color. As anchors they would otherwise
take the browser's blue link color and read as a different species from
the wallet rows beside them.

The zero-wallet greeting is cut to one line. It explained the browser
registration model directly above the options that answer it, which only
pushed them down the screen. The section heading is dropped in that case
too, since the greeting already asks the question; it stays when wallets
are listed and the rows need a label.

Addresses #166.
With nothing in the hint list, a wallet app installed on the device is the
likeliest thing the user actually has -- there is no registered wallet to
prefer, and the QR code needs a second device. Marking that row as the
recommended one gives the user a default to act on rather than three
equal-looking options and no basis for choosing.

Conditional on the hint list being empty. When wallets are listed, one of
them is the likelier choice, so both link rows stay neutral and the list
keeps its precedence.

Styled as a left accent bar over a lightly tinted background, with a dark
mode variant, rather than the filled `wrm-primary` treatment used for
dialog buttons: at full row width that would overpower the section and
compete with the wallet list beside it.

Addresses #166.
A `web+` scheme resolves only to a handler registered through
`navigator.registerProtocolHandler()`. Safari has never shipped that API,
so in Safari the link cannot resolve at all -- and rather than failing
silently the way an unclaimed native scheme does, it reports "the address
is invalid". The row was offering a guaranteed error on every iPhone.

Hidden by feature detection rather than a platform check: the API's
presence is exactly the condition that decides whether the link can work,
and needs no revision if Safari ships it or another engine drops it. On
Android, where Chrome implements the API, both rows still appear.

The failure note reads "this link" when only one row is shown, since
"these links" would name something not on screen.

The zero-wallet greeting no longer promises a wallet to open when neither
row renders -- a desktop Safari has no app link and no web link -- and
points at the QR code instead.

Addresses #166.
Testing the mediator from a phone means serving it under a tunnel
hostname, which requires changing `server.host` and `server.baseUri` --
values in a tracked config that differ per machine.

`configs/dev.js` now imports `configs/local.js` last, when present, so
those overrides win. The file is gitignored; absent it, behavior is
unchanged.
The rows inherited `wrm-flex-row`'s centering, so their icon and text sat
in the middle while the wallet rows above them started at the left edge.
They now start at the left, with a narrower icon column, so the two kinds
of row line up.

The section heading and the silent-failure note are left-justified for the
same reason.

Adds 20px to the cross-device popup height. The dialog does not size
itself to its content, so each section added to it has to be paid for here
by hand; without the extra height the Close button can scroll out of
reach. This is a stopgap until the dynamic sizing work lands.

Addresses #166.
The icons had a fixed 32px width, so when the icon font fails to load the
reserved box renders as an empty square sitting in the row -- the icon's
absence became more visible than the icon. Sizing the element from the
glyph means a missing font costs alignment, not an artifact.

The wallet rows above keep their fixed 48px slot: those hold <img> logos
with their own fallback, not font glyphs.

Addresses #166.
The override is only discoverable by reading `configs/dev.js`, and anyone
using it has to derive the file's shape and the bedrock config keys
involved from scratch.

`configs/local.js.example` is a copyable starting point covering the case
the override exists for: serving the mediator under a tunnel hostname so a
phone can reach it. It carries the reasoning that is easy to get wrong --
why `server.host` and `server.baseUri` are set rather than `server.domain`,
and why the tunnel must point at the HTTPS port rather than the HTTP one,
which would redirect to itself forever. The hostname is a placeholder.

Also corrects the Configuration section, which pointed at
`./configs/authn.localhost.js`. That file is `authn.localhost.js` at the
repository root; the tunable config is `configs/dev.js`.
@applesnort

Copy link
Copy Markdown
Contributor

Reviewed the 10 files unique to this branch (#201's four are covered on that PR). The design reasoning in the comments is good — most of this is the code not matching it yet. Three to fix, then smaller things.

1. The new copy is false in its most common case

showWarning is hints.length === 0 (HintChooser.vue:18), and hints are only registrations that match the request — HintManager.reload() filters through matchCredentialRequest(). So "No wallet is registered in this browser." is wrong precisely when the user has a wallet that doesn't hold the requested credential. The replaced copy handled it with a requestType === 'credentialRequest' variant naming the origin, and this component's other warning branch still draws that distinction — so it now contradicts itself.

Also gone: "or visit your wallet website to register" (a first-timer now gets two rows that assume they already have one, and no route to getting one) and the RP's name.

2. The three harness URLs this PR documents show nothing

?hints=1&link=0, ?hints=1&app=1, ?hints=1&web=0qrEnabled requires qr to be present, and both link URLs are gated on it:

qrEnabled.value && linkEnabled.value && appLinkEnabled.value ? ... : ''

So none render a row, and app=1 can't force one on. Add qr=1 to the examples, or drop qrEnabled from the conjunct if a link row without the QR is a valid state.

3. Activating a row navigates the mediator away with nothing sent

Both anchors are plain <a :href> — no target, no click handler, no emit. Where a web+interaction: handler is registered, the click navigates the mediator's own context to the wallet and crossDevice() never runs. That contradicts "Tapping a row emits nothing and changes no mediator state."

I can't say what the RP sees: the popup path detects the dialog leaving by polling handle.closed (WebAppWindowPopupDialog.js:89-90), which a cross-origin navigation doesn't set, so whether the unload listener above it aborts cleanly needs a real registered handler. Latent today — which is the argument for fixing it now rather than discovering it later. Note target="_blank" alone trades silent failure for a blank tab, so it likely wants pairing with emitting the response on click.

Smaller

  • No test coverage. New app/web/link params, no spec uses them. One row in wallet-chooser.spec.js's STATES table ('hints=0&qr=1&app=1') runs the rows through all four existing invariants. The "152 passed" is a suite that can't see this change — I didn't re-run it, so I'm not disputing the number.
  • The rows don't line up with the wallet list they're meant to match. In the one-wallet screenshot "Demo Wallet 1" starts ~70px right of "Open my wallet app" (48px image slot vs. self-sizing glyph), and the wallet row is inset for Hide while the link row runs full width. The CSS comment's reason for the self-sizing icon is sound — this is just its cost, worth being a decision.
  • failureHint renders before any interaction, and in the one-wallet shot asserts no wallet handles the link with a wallet listed above it. Revealing it on activation says the same thing without talking the user out of their fastest option.
  • One sentence, two owners. HintChooserMessage emits the "Open one you already have:" clause while CrossDeviceOptions suppresses its own heading assuming that. Change either condition and you get a dangling colon or two headings.
  • All rights reserved. deleted from CrossDeviceOptions.vue's header.
  • Dead CSS: .wallet-link.wrm-button — the template no longer applies wrm-button — plus an orphaned comment about links sitting "side by side".
  • Non-blocking: pass {app, web} through as one walletLinks prop rather than flattening it across seven files, given constants.js anticipates renames; have the harness import IS_MOBILE_DEVICE/SUPPORTS_WEB_WALLET_LINK instead of re-implementing them; (any-hover: none) probably excludes an iPad with a keyboard, against "phone / tablet: shown"; the recommended row has no word saying so, and #4a90d9 is a fourth blue next to @wrm-blue and @wrm-primary.

Checked clean: getInteractionUrl() validates https + iuv=1, so the prefix can't yield a javascript: href; ?.app || '' means the String props never see null; - 2026-08-dd matches convention.

Pre-existing, not this PR

  • ThirdPartyMediatorWizard.vue:19 calls a handler that doesn't existsetup() never defines or returns crossDevice (FirstPartyMediatorWizard.vue:63 does). Already on main, but it's a TypeError waiting on whichever path reaches it.
  • getInteractionUrl() returns the raw string, not parsed.toString()new URL() strips tabs and newlines, so what ships can differ from what passed validation. No scheme escape possible; normalizing is free.
  • bindAddr = ['0.0.0.0'] shipped in v7.7.0 with no CHANGELOG entry — a release moved the dev server off loopback and the only record is a code comment. Belongs in a 7.7.0 backfill, not here.

Import by file URL, not filesystem path. `import()` takes a URL, so a bare
absolute path truncates at a `#` in any parent directory name -- reporting a
module nobody asked for -- and on Windows parses the drive letter as a URL
scheme. Reproduced both with a `#` in the path; `pathToFileURL` fixes it.

Drop `server.baseUri` from the example. It is the lazily-evaluated template
`https://${server.host}`, so it already follows `host`; setting both ships two
values that must agree, which is the mismatch the surrounding prose warns
about. Verified by removing it: both the tunnel and local origins still serve.

Correct the `bindAddr` justification. `configs/dev.js` hard-assigns
`config.server.bindAddr`, so setting `server.domain` cannot affect it -- the
reason to prefer `server.host` is that `host` appends the port.

Point `configs/dev.js` at `server.host` rather than `server.domain`. That
comment is the first thing a reader opens and it recommended the knob the
example warns against.

Note that the file cannot register bedrock event handlers, since it is
imported after `@bedrock/config-yaml` and would trip that package's
last-import assertion with a message that does not explain itself.

Note that overriding `server.host` stops `@bedrock/express` sending
`Access-Control-Allow-Private-Network`, which it gates on `host` containing
`localhost`. Traffic through the tunnel is unaffected, but a public-origin
coordinator reaching the local mediator directly will start failing Chrome's
private-network preflight while the file is in place.

Name `local.js` as the development override channel, since
`@bedrock/config-yaml` is a third nominal channel with no stated precedence.

Show restricting `server.bindAddr` to loopback in the example. The all-
interfaces default exists for Docker and is documented as such, but nothing
offers the alternative at the point of use.

Correct the README's Node and npm versions, which read v16+/v8+ against an
`engines.node` of `>=24`.
v-if="collapsible"
class="wrm-dark-gray"
style="padding-top: 0.5em">
Already have a wallet? Open it here:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This text is a little confusing because it can show up when the user "already has a wallet that is in the list above where text appears"

Suggested change
Already have a wallet? Open it here:
Have a wallet installed on this device? Open it here:

This suggestion might not be good enough because are Web wallets that only register the web+interaction scheme "installed on the device?" ... perhaps not. We need some better language, we should bring this to others to bikeshed it collectively.

Maybe we want "Don't see your wallet?" to be a simple text that we use for all of the cases -- and then we have buttons for opening a wallet "on this device" and the "QR code" for opening "on another device". That might solve this generally.

<div class="wallet-link-text">
<strong>Open my web wallet</strong>
<div class="wrm-dark-gray wallet-link-caption">
A wallet website you already use

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similarly, this can show up even if there's a "wallet website you already use in the list above where this appears". So we should bikeshed this a bit too.

Emit the out-of-band response when a row is activated. Both rows are plain
anchors, so where a handler is registered the navigation replaces the
mediator's own context and `crossDevice()` never ran -- the relying party
was left waiting on a dialog that no longer exists, and the popup path
cannot detect that because it polls `handle.closed`, which a same-context
navigation never sets. The rows now emit before navigating. Where no
handler is registered the navigation is a no-op, so the dialog stays usable
either way; the two outcomes are indistinguishable from script, so both are
prepared for.

Define `crossDevice` in `ThirdPartyMediatorWizard`. The template bound
`@cross-device` with no handler in `setup()`, so any path reaching it threw.
Present on main, but this branch is the first to give a user a way there.

Correct the zero-wallet copy. `showWarning` is `hints.length === 0` and
hints are filtered through `matchCredentialRequest()`, so an empty list
means nothing registered here holds what was asked for -- not that no
wallet is registered, which is wrong exactly when the user has a wallet
lacking the credential. Restores the request-type distinction, the relying
party's name, and the route to registering a wallet, which a first-timer
needs and which two rows assuming an existing wallet had removed.

Reveal the silent-failure note only after activation. It asserted that no
wallet handles the link while a matching wallet could be listed above it.

Add the wallet link rows to the layout suite, forcing both gates on so the
rows are covered on every project rather than appearing only where the
browser happens to satisfy them.

Fix the documented harness URLs, which omitted `qr=1` and so rendered
nothing at all, and have the harness import `IS_MOBILE_DEVICE` and
`SUPPORTS_WEB_WALLET_LINK` rather than re-implementing them.

Return the parsed interaction URL rather than the raw string, since
`new URL()` strips tabs and newlines and the value that shipped could
differ from the one validated.

Remove the dead `.wallet-link.wrm-button` rule and its orphaned comment.

Addresses #166.
@djscruggs

Copy link
Copy Markdown
Contributor Author

Applied in 67070f5. Verified each claim against the code first; all three blocking ones held, and item 3 was worse than latent.

1. The copy was false

Confirmed: HintManager.reload() filters through matchCredentialRequest(), so hints.length === 0 means "nothing registered here holds what was asked for". Claiming no wallet is registered is wrong in exactly the case you name, and it contradicted the sibling branch three lines down.

Now request-type aware, with the relying party's name and the route to registering a wallet restored:

No wallet registered in this browser has the requested credential. Open a wallet you already have:
Check verifier.example to find out how to obtain it, or visit your wallet website to register.

You were right that two rows assuming an existing wallet, with no route to getting one, is worse for a first-timer than the copy I replaced.

2. The documented harness URLs showed nothing

Confirmed — qrEnabled requires qr to be present and both link URLs conjoin it, so all three examples rendered no rows and app=1 could not force one on. Added qr=1 to each. Kept qrEnabled in the conjunct: the interaction URL is what produces both, so a link row without the QR is not a state the mediator can reach, and the harness should not invent one.

3. Activation navigated away with nothing sent

This was the one worth catching. The rows now emit the out-of-band response on click, before the navigation. Your reasoning about handle.closed is why: a same-context navigation never sets it, so the relying party was left waiting on a dialog that no longer existed, with no signal either way.

I did not add target="_blank" — as you say, that trades silent failure for a blank tab. Emitting on click covers both outcomes, which is the most script can do given they are indistinguishable from here.

While there: ThirdPartyMediatorWizard.vue binds @cross-device but setup() never defined or returned a handler, so any path reaching it threw. Fixed here rather than deferred — it is on main, but this branch is the first to give a user a way to reach it.

Smaller

  • Test coverage. Two rows added to STATES with both gates forced on, so the rows run through all four invariants on every project instead of only where the browser satisfies the gates. Suite is now 212 passed / 28 failed; every failure is the known has no horizontal overflow assertion (7 states × 4 projects), which I confirmed by filtering. Fair hit on the earlier "152 passed" — that suite could not see this change.
  • Failure note revealed only after activation. Verified: 0 before the click, 1 after.
  • Harness duplication removed — it imports IS_MOBILE_DEVICE and SUPPORTS_WEB_WALLET_LINK now, so it cannot drift.
  • getInteractionUrl() returns parsed.toString().
  • Dead CSS and the orphaned comment removed.

Not changed, with reasons

  • All rights reserved. was deleted deliberately. DB convention drops the phrase, and the guidance is to remove it when you are already editing that header for another reason — which the year bump was. Happy to restore it if you read the convention differently.
  • Row alignment with the wallet list. Real, and the cost of the self-sizing icon: the wallet slot is a fixed 48px image, the link icon sizes from its glyph so a missing font leaves no empty square. I would rather keep the robustness and accept the ~70px offset than reserve a box that renders as an artifact when Font Awesome fails — which I hit on a real device during testing. Calling it out as the decision it is.
  • walletLinks as one prop. Agreed it is better shaped; deferred only to keep this diff reviewable. Worth doing when the scheme names change, which constants.js anticipates.
  • (any-hover: none) and iPads. Correct, an iPad with a trackpad reports hover and loses the row. The PR says "phone or tablet"; the gate is narrower than the prose. Left as-is because the alternative — (any-pointer: coarse) alone — pulls in touchscreen laptops, where there is no wallet app to open. Prose is the thing to fix.
  • #4a90d9 as a fourth blue, and no word marking the recommended row. Both fair. The colour should come from a variable, and "Recommended" as a label would say what the accent bar only implies.

Pre-existing

bindAddr shipping in 7.7.0 with no CHANGELOG entry — agreed that belongs in a backfill, not here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants