Profile

Let signed-in users view and edit their profile. Compose your own form field-by-field with <u-profile> + <u-field>, or drop in <u-full-profile> for a complete UI in one tag. Custom attributes, SSO navigation to connected services, and account deletion round it out.

View this guide as Markdown

Composed profile form

u-profile loads and saves the data; each u-field renders one profile field with validation, labels and the right input type (date picker, phone, country select with flags). You own the layout — group fields in any markup you like.

  • Field-by-field composition — u-field per profile attribute, your layout
  • Typed inputs — dates, phone numbers, country select with icons
  • Validation + labels — render-default-label and built-in error handling
<u-signed-in>
  <!-- u-profile loads the user's data; each u-field binds one profile field.
       partial-validation limits validation to the fields rendered here. -->
  <u-profile partial-validation="true">
    <u-field field="salutation" render-default-label="true"></u-field>
    <div class="grid gap-x-4 sm:grid-cols-2">
      <u-field field="first_name" render-default-label="true"></u-field>
      <u-field field="last_name" render-default-label="true"></u-field>
      <u-field field="date_of_birth" required="true" render-default-label="true"></u-field>
      <u-field field="phone_number" render-default-label="true"></u-field>
    </div>
    <u-field field="country_code" country-code-display-option="icon" render-default-label="true"
    ></u-field>

    <u-submit-button
      class-name="mt-4 px-4 py-2 text-white bg-accent hover:bg-accent-hover rounded-lg disabled:opacity-50 w-full cursor-pointer">
      {t('Spara ändringar', 'Save changes')}
    </u-submit-button>
  </u-profile>
</u-signed-in>

<u-signed-in not>
  <p class="text-text-light">
    {t('Logga in på', 'Sign in on the')} <a href="/auth" class="text-primary underline">{t('inloggningssidan', 'Auth page')}</a> {t('för att redigera din profil.', 'to edit your profile.')}
  </p>
</u-signed-in>
Live demo
Spara ändringar

Logga in på inloggningssidan för att redigera din profil.

Full profile in one tag

u-full-profile renders a complete, ready-made profile editor. Use the fields attribute to pick and order fields (including custom_attributes.*), or omit it to show everything the tenant defines. The fastest path to a working account page.

  • One component — complete profile UI with zero markup
  • Field selection — fields="first_name,last_name,…" picks and orders
<u-signed-in>
  <!-- One component, complete profile UI. Limit and order fields with the
       fields attribute — or omit it to render everything the tenant defines. -->
  <u-full-profile
    fields="first_name,last_name,date_of_birth,country_code"
    country-code-display-option="icon"></u-full-profile>
</u-signed-in>

<u-signed-in not>
  <p class="text-text-light">
    {t('Logga in på', 'Sign in on the')} <a href="/auth" class="text-primary underline">{t('inloggningssidan', 'Auth page')}</a> {t('för att se hela profilen', 'to see the full profile')}
    component.
  </p>
</u-signed-in>
Live demo

Logga in på inloggningssidan för att se hela profilen component.

Custom attributes

Tenant-defined custom attributes behave like normal fields via u-field — or use u-raw-field to build fully custom inputs. These sport chips are plain checkboxes bound to a multi-value attribute, styled with CSS only.

  • custom_attributes.* fields — text, select, multi-select, radio groups
  • u-raw-field escape hatch — any UI you can build with native inputs
<u-signed-in>
  <u-profile partial-validation="true">
    <p class="mb-2 text-sm font-medium">{t.member_number}</p>
    <u-field field="custom_attributes.member_number"></u-field>

    <!-- Configured "show, no edit" in Admin — the SDK renders it readonly from the
         form schema. Value is set server-side by the subscription demo workflow. -->
    <p class="mb-2 mt-4 text-sm font-medium">{t.active_membership}</p>
    <u-field field="custom_attributes.active_membership"></u-field>

    <p class="mb-2 mt-4 text-sm font-medium">{t.favorite_nut}</p>
    <u-field field="custom_attributes.favorite_nut"></u-field>

    <u-submit-button
      class-name="mt-4 px-4 py-2 text-white bg-accent hover:bg-accent-hover rounded-lg disabled:opacity-50 w-full cursor-pointer">
      {t.save}
    </u-submit-button>
  </u-profile>
</u-signed-in>

<u-signed-in not>
  <p class="text-text-light">
    {t.signin_hint} <a href="/auth" class="text-primary underline">{t.signin_link}</a> {t.signin_rest}
  </p>
</u-signed-in>

<style is:inline>
  /* Hide the schema default label — we render translated labels above */
  u-field[field^="custom_attributes."]::part(field_label) {
    display: none;
  }
</style>
Live demo

Medlemsnummer

Aktivt medlemskap

Favoritnöt

Spara ändringar

Logga in på inloggningssidan för att redigera din profil.

Header profile icon

The classic header pattern: u-signed-in swaps a sign-in link for an avatar menu with profile and logout entries. Pure CSS hover dropdown — no JS required.

  • Signed-in/out switch — u-signed-in / u-signed-in not
  • u-logout-button anywhere — style it like any menu item
<!-- A header user-menu: avatar button with a hover dropdown.
     u-signed-in / u-signed-in not switch between menu and sign-in link. -->
<div class="flex justify-end">
  <u-signed-in>
    <div class="group relative inline-block">
      <button
        class="flex items-center justify-center rounded-full border border-border bg-white p-2 shadow-sm hover:bg-background-light"
        aria-label="User menu">
        <svg class="h-6 w-6" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
          <circle cx="12" cy="8" r="4"></circle>
          <path d="M4 20c0-3.5 3.5-6 8-6s8 2.5 8 6"></path>
        </svg>
      </button>

      <!-- hover bridge so the menu doesn't close between button and panel -->
      <div class="absolute left-0 right-0 top-full h-2"></div>

      <div
        class="absolute right-0 top-full z-20 mt-2 hidden w-48 rounded-lg border border-border bg-white shadow-xl group-hover:block">
        <a href="/profile" class="block px-4 py-2 text-sm hover:bg-background-light"
          >Profile settings</a
        >
        <div class="border-t border-border"></div>
        <u-logout-button
          class-name="block w-full text-left px-4 py-2 text-sm hover:bg-background-light cursor-pointer">
          Sign out
        </u-logout-button>
      </div>
    </div>
  </u-signed-in>

  <u-signed-in not>
    <a href="/auth" class="btn btn-primary !min-h-0 !px-4 !py-2 text-sm">Sign in</a>
  </u-signed-in>
</div>
Live demo

Hosted Unidy pages & account deletion

u-jump-to-unidy generates a one-time login token and opens Unidy's hosted pages — including the profile page where account deletion lives (an App-Store requirement most integrations need). no-auth skips the token for public pages.

  • Hosted Unidy pages — subscriptions, tickets, terms, profile
  • Account deletion — deep-link to /profile#logindata
<u-signed-in>
  <div class="flex flex-col gap-3">
    <!-- One-time-token jump into Unidy's hosted pages -->
    <u-jump-to-unidy
      path="/subscriptions"
      newtab
      class-name="px-4 py-2 border border-border text-sm font-medium rounded-lg cursor-pointer hover:bg-background-light disabled:opacity-50">
      Manage subscriptions in Unidy
    </u-jump-to-unidy>

    <!-- Pages that need no authentication: add no-auth -->
    <u-jump-to-unidy
      path="/terms_of_use"
      no-auth
      newtab
      class-name="text-sm text-text-light underline cursor-pointer hover:text-text">
      Terms of use (no auth)
    </u-jump-to-unidy>

    <!-- Account deletion lives on Unidy's profile page — deep-link straight to it -->
    <u-jump-to-unidy
      path="/profile#logindata"
      newtab
      class-name="px-4 py-2 bg-danger hover:bg-danger-hover text-white text-sm font-medium rounded-lg cursor-pointer disabled:opacity-50">
      Delete my account
    </u-jump-to-unidy>
  </div>
</u-signed-in>

<u-signed-in not>
  <p class="text-text-light">
    Sign in on the <a href="/auth" class="text-primary underline">Auth page</a> to use hosted-page navigation.
  </p>
</u-signed-in>
Live demo
Manage subscriptions in Unidy Terms of use (no auth) Delete my account

Sign in on the Auth page to use hosted-page navigation.

Connected services

Show which third-party services a user has connected, and let them connect or disconnect in place. The connect flow runs on the /partner page (which holds the u-oauth-provider); this card checks the live connection state via the service_connections endpoint.

  • Connect / disconnect — visual state card with one-click actions
  • service_connections API — live connection state, no local guessing
  • u-signed-in gating — only shown to authenticated users
{clientId ? (
  <>
    <u-signed-in>
      <div id="pfProvider" class="space-y-3" data-client-id={clientId}>

        <!-- Disconnected state -->
        <div id="pfLocked" class="flex items-center justify-between p-4 rounded-xl border border-border bg-background-light">
          <div class="flex items-center gap-3">
            <div class="w-9 h-9 rounded-lg bg-[#001a3d] flex items-center justify-center flex-shrink-0">
              <img src="/images/djurgardens-logo.svg" alt="" class="w-7 h-7" />
            </div>
            <div>
              <p class="font-semibold text-sm">Djurgården Fan Portal</p>
              <p class="text-xs text-text-muted">{t('Få personligt matchdagsinnehåll', 'Get personalized matchday content')}</p>
            </div>
          </div>
          <a
            href="/partner"
            class="text-xs font-medium px-3 py-1.5 rounded-lg bg-primary text-white hover:bg-primary-dark transition-colors cursor-pointer no-underline"
          >
            Connect
          </a>
        </div>

        <!-- Connected state -->
        <div id="pfConnected" class="flex items-center justify-between p-4 rounded-xl border border-green-200 bg-green-50">
          <div class="flex items-center gap-3">
            <div class="w-9 h-9 rounded-full bg-green-100 flex items-center justify-center flex-shrink-0">
              <svg class="w-4 h-4 text-green-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M5 13l4 4L19 7"/>
              </svg>
            </div>
            <div>
              <p class="font-semibold text-sm text-green-900">Djurgården Fan Portal</p>
              <p class="text-xs text-green-700">Connected</p>
            </div>
          </div>
          <button
            id="pfDisconnectBtn"
            class="text-xs font-medium cursor-pointer px-3 py-1.5 rounded-lg border border-error-border text-danger hover:bg-error-bg transition-colors disabled:opacity-50"
          >
            Disconnect
          </button>
        </div>

      </div>
    </u-signed-in>

    <u-signed-in not>
      <p class="text-text-muted text-sm">{t('Logga in för att hantera anslutna tjänster.', 'Sign in to manage connected services.')}</p>
    </u-signed-in>
  </>
) : (
  <p class="text-text-muted text-sm">No partner service configured for this demo.</p>
)}

<style>
  #pfConnected { display: none; }
  #pfProvider.pf-connected #pfLocked    { display: none; }
  #pfProvider.pf-connected #pfConnected { display: flex; }
</style>

<script type="module">
  import { getUnidyClient, onAuthChange } from "https://cdn.jsdelivr.net/npm/@unidy.io/sdk@1.9.0/dist/sdk/index.esm.js";

  const provider = document.getElementById("pfProvider");
  const disconnectBtn = document.getElementById("pfDisconnectBtn");
  const PARTNER_CLIENT_ID = provider?.dataset.clientId;

  function showConnected()    { provider?.classList.add("pf-connected"); }
  function showDisconnected() { provider?.classList.remove("pf-connected"); }

  async function getServiceHeaders() {
    const client = getUnidyClient();
    const idToken = await client.oauth.getIdToken();
    return {
      "Authorization": "Token " + client.apiClient.api_key,
      "X-Id-Token": idToken,
    };
  }

  async function checkConnection() {
    if (!PARTNER_CLIENT_ID) return;
    try {
      const client = getUnidyClient();
      const headers = await getServiceHeaders();
      const resp = await fetch(client.apiClient.baseUrl + "/api/sdk/v1/service_connections", { headers });
      if (!resp.ok) { showDisconnected(); return; }
      const services = await resp.json();
      const connected = Array.isArray(services) && services.some((s) => s.id === PARTNER_CLIENT_ID);
      connected ? showConnected() : showDisconnected();
    } catch {
      showDisconnected();
    }
  }

  disconnectBtn?.addEventListener("click", async () => {
    if (!PARTNER_CLIENT_ID) return;
    disconnectBtn.disabled = true;
    try {
      const client = getUnidyClient();
      const headers = await getServiceHeaders();
      const resp = await fetch(
        client.apiClient.baseUrl + "/api/sdk/v1/service_connections/" + encodeURIComponent(PARTNER_CLIENT_ID),
        { method: "DELETE", headers }
      );
      if (!resp.ok) throw new Error("HTTP " + resp.status);
      showDisconnected();
    } catch {
      /* keep connected state on failure */
    } finally {
      disconnectBtn.disabled = false;
    }
  });

  // Poll until u-config is initialized (waitForConfig is not exported by the SDK)
  await new Promise(resolve => {
    const iv = setInterval(() => {
      try { getUnidyClient(); clearInterval(iv); resolve(); } catch {}
    }, 50);
  });
  checkConnection();
  onAuthChange(checkConnection);
</script>
Live demo

Djurgården Fan Portal

Få personligt matchdagsinnehåll

Connect

Djurgården Fan Portal

Connected

Logga in för att hantera anslutna tjänster.