# @unqtech/age-verification-mitid

Frontend SDK for age verification using UNQVerify (MitID-powered).

🔗 Official site: [aldersverificering.dk](https://www.aldersverificering.dk)

---

### Contents

- [Installation](#installation)
- [Quick start (npm)](#quick-start-npm)
- [Outcome codes](#outcome-codes)
- [`init(config)` options](#initconfig-options)
- [Other functions](#other-functions)
- [Backwards compatibility](#backwards-compatibility)
- [CDN / No-bundler usage](#cdn--no-bundler-usage)
- [WooCommerce integration](#woocommerce-integration)
- [Security](#security)
- [License](#license)

---

## Installation

```bash
npm install @unqtech/age-verification-mitid
```

TypeScript users can also import the types directly:

```ts
import type { UnqVerifyConfig, VerificationErrorCode, VerificationOutcome } from '@unqtech/age-verification-mitid'
```

---

## Quick start (npm)

```ts
import { init, startVerificationWithRedirect, isVerified } from '@unqtech/age-verification-mitid'

init({
  publicKey:   'pk_test_abc123',
  ageToVerify: 18,
  redirectUri: 'https://yourapp.com/verify-result',

  onVerified:  (payload) => console.log('Verified!', payload),
  onDenied:    (outcome) => alert('You do not meet the age requirement.'),
  onCancelled: (outcome) => console.info('Cancelled:', outcome.code),
  onError:     (outcome) => console.error('Error:', outcome.code, outcome.message),
})

if (!isVerified()) {
  startVerificationWithRedirect()
  // or: startVerificationWithPopup()
}
```

On the page your `redirectUri` points to:

```ts
import { handleRedirectResult } from '@unqtech/age-verification-mitid'

await handleRedirectResult({
  onVerified: (payload) => { /* redirect user back */ },
  onDenied:   (outcome) => { /* show denied message */ },
  onError:    (outcome) => console.error(outcome.code, outcome.message),
  targetOrigin: 'https://yourapp.com', // recommended in popup mode
})
```

---

## Outcome codes

Every non-success path provides a `VerificationOutcome` object:

```ts
interface VerificationOutcome {
  code: VerificationErrorCode  // stable, machine-readable
  message: string
  raw?: unknown
}
```

| Code | Callback | When |
|---|---|---|
| `UNDER_AGE` | `onDenied` | MitID returned `verification_result: false`. Covers both genuine under-age and user opting out at the MitID screen — MitID sends the same response for both. |
| `USER_CANCELLED` | `onCancelled` | User cancelled inside the MitID flow |
| `POPUP_CLOSED` | `onCancelled` | User closed the popup window manually |
| `POPUP_TIMEOUT` | `onCancelled` | Popup open > 5 minutes without result |
| `POPUP_BLOCKED` | `onError` | Browser blocked the popup |
| `NETWORK_ERROR` | `onError` | API call failed |
| `TOKEN_INVALID` | `onError` | JWT missing, expired, or invalid signature |
| `UNTRUSTED_ORIGIN` | `onError` | postMessage received from an untrusted origin |
| `UNKNOWN_ERROR` | `onError` | Unexpected error |

---

## `init(config)` options

| Option | Type | Required | Description |
|---|---|---|---|
| `publicKey` | `string` | yes | Your `pk_test_...` or `pk_live_...` key |
| `ageToVerify` | `number` | yes | Minimum age threshold |
| `redirectUri` | `string` | yes | HTTPS URL the user returns to after MitID |
| `onVerified` | `(payload) => void` | yes | Called on successful verification |
| `onDenied` | `(outcome) => void` | no | Age requirement not met |
| `onCancelled` | `(outcome) => void` | no | Popup closed or timed out |
| `onError` | `(outcome) => void` | no | Technical/system error |
| `onFailure` | `(error?) => void` | no | Legacy catch-all (always invoked alongside granular callbacks) |
| `debug` | `boolean` | no | Verbose SDK logging (default `false`) |

---

## Other functions

| Function | Description |
|---|---|
| `startVerificationWithRedirect()` | Full-page redirect flow |
| `startVerificationWithPopup(popup?)` | Popup flow |
| `handleRedirectResult(opts)` | Process result on the redirect page |
| `isVerified()` | `true` if user has a valid, non-expired token |
| `getVerifiedAge()` | Verified age as `number`, or `null` |
| `resetVerification()` | Clears the session cookie |

---

## Backwards compatibility

`onFailure` is always called alongside any granular callback, so existing `v0.3` integrations continue to work unchanged.

```ts
// v0.3 — still works
onFailure: (err) => showError(err)

// v0.4 — use granular callbacks instead
onDenied:    (outcome) => showMessage('Come back when you are older'),
onCancelled: (outcome) => hideModal(),
onError:     (outcome) => reportError(outcome.code, outcome.raw),
```

---

## CDN / No-bundler usage

Load the self-contained UMD build directly — no npm or build step required.

```html
<script src="https://unpkg.com/@unqtech/age-verification-mitid@latest/dist/index.umd.js"></script>
```

All functions are available under `window.UnqVerify` after the script loads.

---

## WooCommerce integration

### 1 — Enqueue the script (`functions.php`)

```php
add_action( 'wp_enqueue_scripts', function () {
    if ( is_shop() || is_product() || is_product_category() || is_cart() || is_checkout() ) {
        wp_enqueue_script(
            'unqverify-sdk',
            'https://unpkg.com/@unqtech/age-verification-mitid@latest/dist/index.umd.js',
            [], null, true
        );
    }
} );
```

### 2 — Initialize and gate the page

```php
add_action( 'wp_footer', function () {
    if ( ! ( is_shop() || is_product() || is_product_category() || is_cart() || is_checkout() ) ) {
        return;
    }
    ?>
    <script>
    (function () {
        var PUBLIC_KEY   = 'pk_live_YOUR_KEY';
        var REDIRECT_URI = '<?php echo esc_js( home_url( '/aldersverificering/' ) ); ?>';

        if ( window.location.pathname.indexOf( '/aldersverificering' ) !== -1 ) {
            UnqVerify.init({
                publicKey:   PUBLIC_KEY,
                ageToVerify: 18,
                redirectUri: REDIRECT_URI,
                onVerified: function () {
                    var returnTo = sessionStorage.getItem( 'unqverify_return' ) || '<?php echo esc_js( wc_get_page_permalink( "shop" ) ); ?>';
                    window.location.href = returnTo;
                },
                onDenied: function () {
                    alert( 'Du opfylder desværre ikke alderskravet.' );
                },
                onError: function () {
                    alert( 'Aldersverificering mislykkedes. Prøv venligst igen.' );
                }
            });
            UnqVerify.handleRedirectResult({
                onVerified: function () {},
                onDenied:   function ( outcome ) { console.warn( outcome.code ); },
                onError:    function ( outcome ) { console.error( outcome.code ); }
            });
            return;
        }

        UnqVerify.init({
            publicKey:   PUBLIC_KEY,
            ageToVerify: 18,
            redirectUri: REDIRECT_URI,
            onVerified:  function () {},
            onError:     function () {}
        });

        if ( ! UnqVerify.isVerified() ) {
            sessionStorage.setItem( 'unqverify_return', window.location.href );
            UnqVerify.startVerificationWithRedirect();
        }
    })();
    </script>
    <?php
} );
```

### 3 — Create the verification result page

1. Go to **Pages → Add New** in WordPress admin
2. Set the slug to `aldersverificering` (matching `REDIRECT_URI` above)
3. Leave the content empty — the script handles everything
4. Register the URL as an **Allowed redirect URI** in your UNQVerify dashboard

---

## Security

- All JWTs are RS256-verified against the issuer's JWKS endpoint
- Unknown issuers are always rejected
- `postMessage` events are validated against a strict origin allowlist
- The `unqverify_token` cookie is set with `SameSite=Lax; Secure`
- Cookie lifetime is the greater of the JWT `exp` claim or **1 hour**, ensuring the verification session is not lost too quickly even if the token has a short TTL

JWKS endpoints:
```
https://test.api.aldersverificering.dk/well-known/openid-configuration/jwks  (test)
https://api.aldersverificering.dk/well-known/openid-configuration/jwks        (live)
```

---

## License

MIT © UNQTech ApS
