# Unidy Web SDK — 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.

## Setup

Every example below assumes the SDK is loaded and configured once per page:

```html
<script type="module" src="https://cdn.jsdelivr.net/npm/@unidy.io/sdk@1.8.1/dist/sdk/sdk.esm.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@unidy.io/sdk@1.8.1/dist/sdk/sdk.css">

<u-config
  base-url="https://fis.staging.unidy.app"
  api-key="your-api-key"
  locale="en"
  check-signed-in="true"></u-config>
```

## 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

```html
<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">
      Save changes
    </u-submit-button>
  </u-profile>
</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 edit your profile.
  </p>
</u-signed-in>
```

Live demo: /profile#profile-form

## 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

```html
<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">
    Sign in on the <a href="/auth" class="text-primary underline">Auth page</a> to see the full profile
    component.
  </p>
</u-signed-in>
```

Live demo: /profile#full-profile

## 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

```html
<u-signed-in>
  <u-profile partial-validation="true">
    <u-field field="custom_attributes.fav_athlete" render-default-label="true"></u-field>

    <p class="mb-2 mt-4 text-sm font-medium">Favorite disciplines</p>
    <div class="flex flex-wrap gap-2">
      <label class="discipline-chip">
        <u-raw-field field="custom_attributes.fav_discipline" type="checkbox" value="slalom" class-name="sr-only"></u-raw-field>
        Slalom
      </label>
      <label class="discipline-chip">
        <u-raw-field field="custom_attributes.fav_discipline" type="checkbox" value="giant_slalom" class-name="sr-only"></u-raw-field>
        Giant Slalom
      </label>
      <label class="discipline-chip">
        <u-raw-field field="custom_attributes.fav_discipline" type="checkbox" value="super_g" class-name="sr-only"></u-raw-field>
        Super-G
      </label>
      <label class="discipline-chip">
        <u-raw-field field="custom_attributes.fav_discipline" type="checkbox" value="downhill" class-name="sr-only"></u-raw-field>
        Downhill
      </label>
      <label class="discipline-chip">
        <u-raw-field field="custom_attributes.fav_discipline" type="checkbox" value="freestyle" class-name="sr-only"></u-raw-field>
        Freestyle
      </label>
      <label class="discipline-chip">
        <u-raw-field field="custom_attributes.fav_discipline" type="checkbox" value="ski_jumping" class-name="sr-only"></u-raw-field>
        Ski Jumping
      </label>
    </div>

    <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">
      Save changes
    </u-submit-button>
  </u-profile>
</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 edit your profile.
  </p>
</u-signed-in>

<style is:inline>
  .discipline-chip {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 90px;
    height: 2.5rem;
    padding: 0 0.75rem;
    border: 1px solid var(--color-border);
    border-radius: 9999px;
    background: var(--color-white);
    font-size: 0.875rem;
    cursor: pointer;
    transition: all var(--transition-normal);
  }
  .discipline-chip:has(u-raw-field input:checked) {
    background: var(--color-primary-soft);
    border-color: var(--color-primary);
    color: var(--color-primary-dark);
  }
</style>

<script is:inline>
  /* After u-profile saves, the form resets u-raw-field checkboxes.
     Capture the checked values before submit and restore them after. */
  (function () {
    var fields = document.querySelectorAll('u-raw-field[field="custom_attributes.fav_discipline"]');
    if (!fields.length) return;

    var saved = [];

    function getChecked() {
      return Array.from(fields).filter(function (rf) {
        var inp = rf.querySelector('input');
        return inp && inp.checked;
      }).map(function (rf) { return rf.getAttribute('value'); });
    }

    function restore() {
      fields.forEach(function (rf) {
        var inp = rf.querySelector('input');
        if (!inp) return;
        var should = saved.includes(rf.getAttribute('value'));
        if (inp.checked !== should) {
          inp.checked = should;
          inp.dispatchEvent(new Event('change', { bubbles: true }));
        }
      });
    }

    document.addEventListener('click', function (e) {
      var el = e.target;
      for (var i = 0; i < 6; i++) {
        if (!el || el === document.body) break;
        if ((el.tagName || '').toLowerCase() === 'u-submit-button') {
          saved = getChecked();
          setTimeout(restore, 400);
          setTimeout(restore, 900);
          break;
        }
        el = el.parentElement;
      }
    }, true);
  })();
</script>
```

Live demo: /profile#custom-attributes

## 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

```html
<!-- 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: /profile#profile-icon

## SSO navigation & account deletion

u-jump-to-service signs the user into another OAuth app of your tenant; u-jump-to-unidy generates a one-time login token and opens Unidy's hosted pages — including the profile page where account deletion lives. no-auth skips the token for public pages.

- **Service SSO** — jump into connected apps without re-login
- **Hosted Unidy pages** — subscriptions, tickets, terms, profile
- **Account deletion** — deep-link to /profile#logindata

```html
<u-signed-in>
  <div class="flex flex-col gap-3">
    <!-- SSO jump to the FIS Fan Portal (service-id = 6) -->
    <u-jump-to-service
      service-id="6"
      newtab
      class-name="px-4 py-2 bg-accent hover:bg-accent-hover text-white text-sm font-medium rounded-lg cursor-pointer disabled:opacity-50">
      Open FIS Fan Portal
    </u-jump-to-service>

    <!-- 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 SSO navigation.
  </p>
</u-signed-in>
```

Live demo: /profile#jump-and-delete

## 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 UI reads the result from sessionStorage and shows a connected or disconnected state accordingly.

- **Connect / disconnect** — visual state card with one-click actions
- **sessionStorage handoff** — state set on /partner, read here
- **u-signed-in gating** — only shown to authenticated users

```html
{clientId ? (
  <>
    <u-signed-in>
      <div id="pfProvider" class="space-y-3">

        <!-- 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-[#1a1f2e] flex items-center justify-center flex-shrink-0">
              <svg width="18" height="18" viewBox="0 0 28 28" fill="none">
                <path d="M14 2v24M2 14h24M5.5 5.5l17 17M22.5 5.5l-17 17" stroke="white" stroke-width="2.5" stroke-linecap="round"/>
              </svg>
            </div>
            <div>
              <p class="font-semibold text-sm">FIS Fan Portal</p>
              <p class="text-xs text-text-muted">Get personalized race 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">FIS 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"
          >
            Disconnect
          </button>
        </div>

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

    <u-signed-in not>
      <p class="text-text-muted text-sm">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 is:inline>
  (function () {
    var provider = document.getElementById('pfProvider');
    if (!provider) return;

    function showConnected() {
      provider.classList.add('pf-connected');
      try { sessionStorage.setItem('fis_connected', '1'); } catch (_) {}
    }

    function showDisconnected() {
      provider.classList.remove('pf-connected');
      try { sessionStorage.removeItem('fis_connected'); } catch (_) {}
    }

    // Restore from sessionStorage (set by /partner page after successful connect)
    try { if (sessionStorage.getItem('fis_connected') === '1') showConnected(); } catch (_) {}

    var btn = document.getElementById('pfDisconnectBtn');
    if (btn) btn.addEventListener('click', showDisconnected);
  })();
</script>
```

Live demo: /profile#service-connect

## Further reading

- Full component reference: https://github.com/UnidyID/unidy-sdk/blob/HEAD/packages/sdk/readme.md
- Official quick-start examples: https://github.com/UnidyID/unidy-sdk/blob/HEAD/packages/sdk/quick-start-examples.md
- Package: https://www.npmjs.com/package/@unidy.io/sdk
- All guides: /llms.txt
