PhoenixKitWeb.Components.Core.ConnectAccountButton (phoenix_kit v2.3.0)

Copy Markdown View Source

A "Connect your X account" button that runs an OAuth authorization in a popup window — the Google-login-style pattern for third-party integrations (distinct from OAuthProvider, which handles sign-in to the app itself).

Flow contract:

  1. The button opens href in a named popup (the app's OAuth start route, which redirects to the provider's consent page).
  2. The provider redirects back to the app's callback route inside the popup; the callback completes the exchange server-side.
  3. The callback page refreshes the opener and closes itself — see "Callback page" below.

Progressive enhancement

This renders a real <a href>, and the popup is opened by the PopupLink JS hook rather than an inline onclick (the kit removed inline handlers everywhere for CSP). The click is intercepted only after window.open actually returns a window, so a blocked popup, JS being off, or a modifier-click all fall back to ordinary navigation and the flow still completes full-page. The previous inline version ended every click with return false, which cancelled navigation even when the popup never opened — the button did nothing.

Host wiring: the hook ships in core's phoenix_kit.js, which hosts already load and spread into their LiveSocket (mix phoenix_kit.install wires this). No per-app JS.

Callback page

The popup hands control back to the opener. On the callback page:

<script>
  if (window.opener && !window.opener.closed) {
    window.opener.location.reload();
  }
  window.close();
</script>

The popup is deliberately not rel="noopener": window.opener is exactly what the callback needs in order to refresh the page behind it.

Be clear-eyed about what that costs. The popup navigates on to the provider's consent page, and window.opener survives navigation — so the provider's origin holds a live (cross-origin) reference to your window. The same-origin policy limits it to opener.location = …, postMessage, close() and focus(), but the first of those is reverse tabnabbing: a compromised or open-redirect-chained provider page can navigate the tab behind the popup.

href is still required to be a local path — that stops the reference being handed straight to an arbitrary origin, and keeps the flow starting on a route you control — but it is not what makes the popup safe once the provider takes over.

Note that Cross-Origin-Opener-Policy: same-origin-allow-popups does NOT help here: that value exists precisely to keep the opener relationship for popups, which is the relationship in question. If the provider is inside your threat model, the only real fix is to stop relying on window.opener at all — open with noopener and have the callback route broadcast on PubSub so the opener LiveView updates itself. That is the Phoenix-native version of this flow and needs no opener reference or location.reload().

Origin: extracted from NordSwitch's Shelly account connect flow; intended for any Integrations-system OAuth (Google, Stripe, …).

Summary

Functions

Renders the connect-account popup button.

Functions

connect_account_button(assigns)

Renders the connect-account popup button.

Examples

<.connect_account_button href="/shelly/oauth/start">
  Connect Shelly account
</.connect_account_button>

<.connect_account_button
  href={~p"/integrations/google/start"}
  class="btn btn-outline btn-sm"
  window_name="google-connect"
>
  Connect Google
</.connect_account_button>

Attributes

  • href (:string) (required) - The app's OAuth start route. Must be a local path — the popup keeps its window.opener, so it is never pointed at a third-party origin.
  • window_name (:string) - Popup window name. Defaults to one derived from href, so two buttons on a page (Connect Google / Connect Shelly) get distinct windows AND distinct DOM ids without configuration. Sharing a name reuses the same window. Defaults to nil.
  • window_width (:integer) - Defaults to 480.
  • window_height (:integer) - Defaults to 680.
  • class (:any) - REPLACES the default styling rather than merging with it, so you can swap btn-primary for btn-outline without a specificity fight. Pass the full class list you want. Defaults to "btn btn-primary btn-sm".
  • id (:string) - DOM id. A phx-hook element MUST have one, so it defaults to the window name — pass an explicit id when two buttons share a window name. Defaults to nil.
  • Global attributes are accepted. Extra attributes for the anchor (aria-, data-, …).

Slots

  • inner_block (required)