@atlasauth/svelte 0.9.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # @atlasauth/svelte changelog
2
2
 
3
+ ## 0.11.0 — 2026-10-24
4
+
5
+ - Integrator platform release: see the Atlas changelog for 2026-10-24.
6
+
7
+ ## 0.10.0 — 2026-10-24
8
+
9
+ ### Added
10
+
11
+ - **Policy acceptance** in `<SignUp>` and `<SignIn>` (consent block with in-page viewer, clickwrap line, unskippable scroll dialog, the `needs_policy_acceptance` step), plus `<PolicyGate>`, `<PolicyConsent>`, `<PolicyClickwrap>`, `<PolicyAcceptStep>`; `signUp()` exposes `policies`. New `policy.*` strings.
12
+
3
13
  ## 0.9.0 — 2026-10-23
4
14
 
5
15
  ### Added
@@ -1,5 +1,5 @@
1
1
  import { type Readable } from 'svelte/store';
2
- import { type OrganizationAuditLogOptions, type OrganizationAuditLogState, type CaptchaController, type FlowState, type InstanceSettings, type ProviderToken, type SignOutOptions, type SignOutResult, type MfaChoices, type MfaEnrollmentController, type MfaEnrollmentState } from '@atlasauth/js';
2
+ import { type OrganizationAuditLogOptions, type OrganizationAuditLogState, type CaptchaController, type FlowState, type InstanceSettings, type ProviderToken, type SignOutOptions, type SignOutResult, PolicyAcceptanceController, type MfaChoices, type MfaEnrollmentController, type MfaEnrollmentState } from '@atlasauth/js';
3
3
  import { type AuthStatus } from './context';
4
4
  import { type ProtectCondition } from './protect';
5
5
  import type { AtlasOrganizationMembership, AtlasSession, AtlasUser } from './types';
@@ -165,6 +165,15 @@ export interface FlowController {
165
165
  captcha: CaptchaController;
166
166
  /** The sign-up captcha (the create-account step). */
167
167
  signUpCaptcha: CaptchaController;
168
+ /**
169
+ * The instance's policies (terms, privacy, …) for the create-account form:
170
+ * `create` sends their acceptances and reloads them after a refusal.
171
+ */
172
+ policies: PolicyAcceptanceController;
173
+ /** Load the sign-up's policies into {@link policies} (`<SignUp>` calls it on mount). */
174
+ loadPolicies(): Promise<void>;
175
+ /** The user declined a policy a sign-in was waiting on: cancels the attempt and says why. */
176
+ declinePolicy(title: string): void;
168
177
  /** Stop the magic-link poll loop and remove the captcha widgets — call from `onDestroy`. */
169
178
  destroy(): void;
170
179
  }
package/dist/accessors.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { derived, get, readable, writable } from 'svelte/store';
2
- import { FapiClient, advanceWithCaptcha, atlasCaptchaLabels, createCaptchaController, createPasskeySignUp, createSignUp, finishPasskeySignUp, fetchInstanceSettings, fetchLastSignInMethod, flowNeedsCaptcha, signInCaptchaOptions, flowFromPending, pollAttempt, prepareFactor, shouldKeepPolling, createOrganizationAuditLog, createMfaEnrollment, acknowledgeRecoveryCodes, pendingRecoveryCodes, mfaChoicesFrom, prepareSecondFactorCode, resendSignUpVerification, } from '@atlasauth/js';
2
+ import { FapiClient, advanceWithCaptcha, atlasCaptchaLabels, createCaptchaController, createPasskeySignUp, createSignUp, finishPasskeySignUp, fetchInstanceSettings, fetchLastSignInMethod, flowNeedsCaptcha, signInCaptchaOptions, flowFromPending, pollAttempt, prepareFactor, shouldKeepPolling, createOrganizationAuditLog, createMfaEnrollment, acknowledgeRecoveryCodes, pendingRecoveryCodes, mfaChoicesFrom, prepareSecondFactorCode, resendSignUpVerification, PolicyAcceptanceController, loadPolicies as loadPolicyList, initialFlow as emptyFlow, } from '@atlasauth/js';
3
3
  import { useAtlas } from './context';
4
4
  import { translate } from './i18n';
5
5
  import { evaluate } from './protect';
@@ -191,16 +191,23 @@ function createFlow() {
191
191
  await finish(out.state);
192
192
  })();
193
193
  }
194
+ const policies = new PolicyAcceptanceController([]);
195
+ async function loadPolicies() {
196
+ policies.setPolicies(await loadPolicyList(client, { locale: atlas.locale ?? undefined }));
197
+ }
198
+ function declinePolicy(title) {
199
+ state.set({ ...emptyFlow, errors: [{ code: 'POLICY_DECLINED', message: translate(atlas.catalog, 'policy.declined', { policy: title }) }] });
200
+ }
194
201
  function create(values, consent = true) {
195
202
  void (async () => {
196
203
  state.update((current) => ({ ...current, busy: true, errors: [] }));
197
- await finish(await createSignUp(client, values, { consent, captcha: signUpCaptcha, t }));
204
+ await finish(await createSignUp(client, values, { consent, captcha: signUpCaptcha, t, policies }));
198
205
  })();
199
206
  }
200
207
  function createWithPasskey(values, consent = true) {
201
208
  void (async () => {
202
209
  state.update((current) => ({ ...current, busy: true, errors: [] }));
203
- await finish(await createPasskeySignUp(client, values, { consent, captcha: signUpCaptcha, t }));
210
+ await finish(await createPasskeySignUp(client, values, { consent, captcha: signUpCaptcha, t, policies }));
204
211
  })();
205
212
  }
206
213
  async function sendSecondFactorCode(strategy) {
@@ -280,6 +287,9 @@ function createFlow() {
280
287
  lastMethod: { subscribe: lastMethod.subscribe },
281
288
  captcha,
282
289
  signUpCaptcha,
290
+ policies,
291
+ loadPolicies,
292
+ declinePolicy,
283
293
  destroy,
284
294
  };
285
295
  }
@@ -31,7 +31,7 @@ export interface DesignTokens {
31
31
  spacingUnit?: string;
32
32
  }
33
33
  /** Every element a customer may target. Named for what it IS, not where it sits. */
34
- export type ElementKey = 'root' | 'card' | 'headerTitle' | 'headerSubtitle' | 'formField' | 'formFieldLabel' | 'formFieldInput' | 'formFieldError' | 'formButtonPrimary' | 'formButtonSecondary' | 'socialButton' | 'dividerText' | 'footerAction' | 'avatar' | 'menu' | 'menuItem' | 'list' | 'listItem' | 'badge';
34
+ export type ElementKey = 'root' | 'card' | 'headerTitle' | 'headerSubtitle' | 'formField' | 'formFieldLabel' | 'formFieldInput' | 'formFieldError' | 'formButtonPrimary' | 'formButtonSecondary' | 'socialButton' | 'dividerText' | 'footerAction' | 'avatar' | 'menu' | 'menuItem' | 'list' | 'listItem' | 'badge' | 'alert' | 'text' | 'link';
35
35
  export interface Appearance extends DesignAppearance {
36
36
  /** A prebuilt token theme to start from (pre-design-system themes). */
37
37
  baseTheme?: keyof typeof THEMES;
@@ -23,7 +23,7 @@ export const DEFAULT_TOKENS = {
23
23
  colorPrimary: '#5b5bd6',
24
24
  colorText: '#1c1c1f',
25
25
  colorBackground: '#ffffff',
26
- colorDanger: '#d64545',
26
+ colorDanger: '#c53030',
27
27
  colorBorder: '#e4e4e9',
28
28
  borderRadius: '8px',
29
29
  fontFamily: 'ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif',
@@ -11,6 +11,7 @@ import { useAtlas } from "../context";
11
11
  import { classFor } from "../appearance";
12
12
  import { translate } from "../i18n";
13
13
  import CaptchaSlot from "./CaptchaSlot.svelte";
14
+ import PolicyAcceptStep from "./PolicyAcceptStep.svelte";
14
15
  export let attempt = null;
15
16
  export let busy = false;
16
17
  export let errors = [];
@@ -26,6 +27,7 @@ export let enrollment = null;
26
27
  export let enrollmentState = null;
27
28
  export let emailVerification = "code";
28
29
  export let onResend = null;
30
+ export let onPolicyDecline = null;
29
31
  const atlas = useAtlas();
30
32
  const cls = (key, base) => classFor(key, atlas.appearance, base);
31
33
  const t = (key, vars) => translate(atlas.catalog, key, vars);
@@ -124,12 +126,13 @@ $: message = (() => {
124
126
  case "enroll_second_factor":
125
127
  case "collect_email_code":
126
128
  case "collect_new_password":
129
+ case "accept_policies":
127
130
  return null;
128
131
  default:
129
132
  return t("error.generic");
130
133
  }
131
134
  })();
132
- $: showSubmit = (!enrolling || enrolSubmitKey !== null) && step.kind !== "done" && step.kind !== "await_oauth" && message === null && !linkOnly && // Only emailed factors on offer: the option buttons are the way forward.
135
+ $: showSubmit = (!enrolling || enrolSubmitKey !== null) && step.kind !== "done" && step.kind !== "await_oauth" && step.kind !== "accept_policies" && message === null && !linkOnly && // Only emailed factors on offer: the option buttons are the way forward.
133
136
  (step.kind !== "collect_first_factor" || fields.length > 0);
134
137
  $: showEmailOptions = step.kind === "collect_first_factor" && message === null && sent === null && emailOptions.length > 0 && !(sending && busy);
135
138
  $: showCaptcha = !!captcha && (step.kind === "collect_identifier" || step.kind === "collect_captcha");
@@ -164,6 +167,10 @@ function handleSubmit(event) {
164
167
  <p>{message}</p>
165
168
  {/if}
166
169
  {:else}
170
+ {#if step.kind === 'accept_policies'}
171
+ <!-- A required policy changed: scroll-mode ones in their dialog, the rest as checkboxes. -->
172
+ <PolicyAcceptStep {attempt} {busy} {onSubmit} onDecline={(policy) => onPolicyDecline?.(policy.title)} />
173
+ {/if}
167
174
  {#if step.kind === 'collect_captcha'}
168
175
  <!-- §5 the server withheld the attempt until a captcha clears. Solving
169
176
  the widget resubmits the identifier on its own; the button retries. -->
@@ -54,6 +54,7 @@ declare const __propDef: {
54
54
  * while the flow polls, and offers a resend (`onResend`).
55
55
  */ emailVerification?: string;
56
56
  onResend?: (() => Promise<boolean>) | null;
57
+ /** The user declined a policy the sign-in was waiting on (`needs_policy_acceptance`): cancel the attempt. */ onPolicyDecline?: ((title: string) => void) | null;
57
58
  };
58
59
  events: {
59
60
  [evt: string]: CustomEvent<any>;
@@ -0,0 +1,52 @@
1
+ <script>import { onMount } from "svelte";
2
+ import {
3
+ FapiClient,
4
+ controllerForPending,
5
+ injectPolicyStyles,
6
+ mountPolicyAcceptStep
7
+ } from "@atlasauth/js";
8
+ import { useAtlas } from "../context";
9
+ import { usePolicyUi } from "../policies";
10
+ import { ATLAS_SVELTE_SDK } from "../version";
11
+ export let attempt = null;
12
+ export let busy = false;
13
+ export let onSubmit;
14
+ export let onDecline = void 0;
15
+ const atlas = useAtlas();
16
+ const ui = usePolicyUi();
17
+ let controller = null;
18
+ let step = null;
19
+ function host(node, c) {
20
+ injectPolicyStyles(node.ownerDocument);
21
+ step = mountPolicyAcceptStep(node, c, {
22
+ ...ui,
23
+ onSubmit: (acceptances) => onSubmit({ policy_acceptances: JSON.stringify(acceptances) }),
24
+ onDecline: (policy) => onDecline?.(policy)
25
+ });
26
+ step.setBusy(busy);
27
+ return {
28
+ destroy() {
29
+ step?.destroy();
30
+ step = null;
31
+ }
32
+ };
33
+ }
34
+ $: step?.setBusy(busy);
35
+ onMount(() => {
36
+ const client = new FapiClient({
37
+ publishableKey: atlas.publishableKey,
38
+ baseUrl: atlas.frontendApi,
39
+ locale: atlas.locale,
40
+ sdk: ATLAS_SVELTE_SDK
41
+ });
42
+ void controllerForPending(client, attempt, { locale: atlas.locale ?? void 0 }).then((c) => {
43
+ controller = c;
44
+ });
45
+ });
46
+ </script>
47
+
48
+ {#if controller}
49
+ <div use:host={controller} />
50
+ {:else}
51
+ <p class="atlas-text">{ui.t('policy.loading')}</p>
52
+ {/if}
@@ -0,0 +1,30 @@
1
+ import { SvelteComponent } from "svelte";
2
+ import { type Policy } from '@atlasauth/js';
3
+ declare const __propDef: {
4
+ props: {
5
+ /**
6
+ * The sign-in step for a `needs_policy_acceptance` attempt: scroll-mode
7
+ * policies in their unskippable dialog, the rest as checkboxes, then "Accept
8
+ * and continue", which calls `onSubmit` with the `policy_acceptances` JSON for
9
+ * the flow. Declining a policy calls `onDecline` (cancel the attempt).
10
+ */ attempt?: {
11
+ id: string | null;
12
+ pending_policies?: unknown;
13
+ } | null;
14
+ busy?: boolean;
15
+ onSubmit: (values: Record<string, string>) => void;
16
+ onDecline?: ((policy: Policy) => void) | undefined;
17
+ };
18
+ events: {
19
+ [evt: string]: CustomEvent<any>;
20
+ };
21
+ slots: {};
22
+ exports?: {} | undefined;
23
+ bindings?: string | undefined;
24
+ };
25
+ export type PolicyAcceptStepProps = typeof __propDef.props;
26
+ export type PolicyAcceptStepEvents = typeof __propDef.events;
27
+ export type PolicyAcceptStepSlots = typeof __propDef.slots;
28
+ export default class PolicyAcceptStep extends SvelteComponent<PolicyAcceptStepProps, PolicyAcceptStepEvents, PolicyAcceptStepSlots> {
29
+ }
30
+ export {};
@@ -0,0 +1,23 @@
1
+ <script>import { injectPolicyStyles, mountPolicyClickwrap } from "@atlasauth/js";
2
+ import { usePolicyUi } from "../policies";
3
+ export let controller;
4
+ const ui = usePolicyUi();
5
+ function host(node, c) {
6
+ injectPolicyStyles(node.ownerDocument);
7
+ let current = c;
8
+ let mounted = mountPolicyClickwrap(node, current, ui);
9
+ return {
10
+ update(next) {
11
+ if (next === current) return;
12
+ mounted.destroy();
13
+ current = next;
14
+ mounted = mountPolicyClickwrap(node, current, ui);
15
+ },
16
+ destroy() {
17
+ mounted.destroy();
18
+ }
19
+ };
20
+ }
21
+ </script>
22
+
23
+ <p use:host={controller} />
@@ -0,0 +1,19 @@
1
+ import { SvelteComponent } from "svelte";
2
+ import { type PolicyAcceptanceController } from '@atlasauth/js';
3
+ declare const __propDef: {
4
+ props: {
5
+ /** The clickwrap sentence ("By continuing you agree to …") for `link`-mode policies, under the submit button. */ controller: PolicyAcceptanceController;
6
+ };
7
+ events: {
8
+ [evt: string]: CustomEvent<any>;
9
+ };
10
+ slots: {};
11
+ exports?: {} | undefined;
12
+ bindings?: string | undefined;
13
+ };
14
+ export type PolicyClickwrapProps = typeof __propDef.props;
15
+ export type PolicyClickwrapEvents = typeof __propDef.events;
16
+ export type PolicyClickwrapSlots = typeof __propDef.slots;
17
+ export default class PolicyClickwrap extends SvelteComponent<PolicyClickwrapProps, PolicyClickwrapEvents, PolicyClickwrapSlots> {
18
+ }
19
+ export {};
@@ -0,0 +1,23 @@
1
+ <script>import { injectPolicyStyles, mountPolicyConsent } from "@atlasauth/js";
2
+ import { usePolicyUi } from "../policies";
3
+ export let controller;
4
+ const ui = usePolicyUi();
5
+ function host(node, c) {
6
+ injectPolicyStyles(node.ownerDocument);
7
+ let current = c;
8
+ let mounted = mountPolicyConsent(node, current, ui);
9
+ return {
10
+ update(next) {
11
+ if (next === current) return;
12
+ mounted.destroy();
13
+ current = next;
14
+ mounted = mountPolicyConsent(node, current, ui);
15
+ },
16
+ destroy() {
17
+ mounted.destroy();
18
+ }
19
+ };
20
+ }
21
+ </script>
22
+
23
+ <div use:host={controller} />
@@ -0,0 +1,24 @@
1
+ import { SvelteComponent } from "svelte";
2
+ import { type PolicyAcceptanceController } from '@atlasauth/js';
3
+ declare const __propDef: {
4
+ props: {
5
+ /**
6
+ * The checkbox-mode policies (terms, privacy, …): one combined checkbox or one
7
+ * each, the names opening the document in an in-page viewer. The state lives in
8
+ * the `controller` (`policies` of `signUp()`, or a `PolicyAcceptanceController`
9
+ * of your own). The DOM is drawn by `@atlasauth/js`, shared with every SDK.
10
+ */ controller: PolicyAcceptanceController;
11
+ };
12
+ events: {
13
+ [evt: string]: CustomEvent<any>;
14
+ };
15
+ slots: {};
16
+ exports?: {} | undefined;
17
+ bindings?: string | undefined;
18
+ };
19
+ export type PolicyConsentProps = typeof __propDef.props;
20
+ export type PolicyConsentEvents = typeof __propDef.events;
21
+ export type PolicyConsentSlots = typeof __propDef.slots;
22
+ export default class PolicyConsent extends SvelteComponent<PolicyConsentProps, PolicyConsentEvents, PolicyConsentSlots> {
23
+ }
24
+ export {};
@@ -0,0 +1,83 @@
1
+ <script>import { onDestroy } from "svelte";
2
+ import {
3
+ FapiClient,
4
+ PolicyGateController,
5
+ formatPolicyDate,
6
+ injectPolicyStyles,
7
+ joinTitles,
8
+ mountPolicyAcceptStep
9
+ } from "@atlasauth/js";
10
+ import { useAtlas } from "../context";
11
+ import { classFor } from "../appearance";
12
+ import { usePolicyUi } from "../policies";
13
+ import { ATLAS_SVELTE_SDK } from "../version";
14
+ export let onAccepted = void 0;
15
+ const atlas = useAtlas();
16
+ const status = atlas.status;
17
+ const ui = usePolicyUi();
18
+ const gate = new PolicyGateController(
19
+ new FapiClient({ publishableKey: atlas.publishableKey, baseUrl: atlas.frontendApi, locale: atlas.locale, sdk: ATLAS_SVELTE_SDK }),
20
+ atlas.locale ?? void 0
21
+ );
22
+ let pending = gate.pending;
23
+ let grace = gate.graceEndsAt;
24
+ let controller = null;
25
+ let error = "";
26
+ let step = null;
27
+ const off = gate.subscribe(() => {
28
+ pending = gate.pending;
29
+ grace = gate.graceEndsAt;
30
+ });
31
+ onDestroy(() => {
32
+ off();
33
+ step?.destroy();
34
+ });
35
+ $: if ($status === "signed_in") void gate.load();
36
+ async function begin() {
37
+ controller = await gate.begin();
38
+ }
39
+ function host(node, c) {
40
+ injectPolicyStyles(node.ownerDocument);
41
+ step = mountPolicyAcceptStep(node, c, {
42
+ ...ui,
43
+ onSubmit: async () => {
44
+ step?.setBusy(true);
45
+ const res = await gate.accept(c);
46
+ step?.setBusy(false);
47
+ if (!res.ok) {
48
+ error = res.errors[0]?.message ?? ui.t("policy.requiredGeneric");
49
+ return;
50
+ }
51
+ controller = null;
52
+ onAccepted?.();
53
+ },
54
+ onDecline: () => {
55
+ controller = null;
56
+ }
57
+ });
58
+ return {
59
+ destroy() {
60
+ step?.destroy();
61
+ step = null;
62
+ }
63
+ };
64
+ }
65
+ </script>
66
+
67
+ {#if pending.length > 0 && !controller}
68
+ <div class={classFor('alert', atlas.appearance, 'atlas-policy-banner')} role="status" data-atlas-policy-banner="">
69
+ <span>
70
+ <strong>{ui.t('policy.bannerTitle')}. </strong>
71
+ {ui.t('policy.bannerBody', { policies: joinTitles(pending.map((p) => p.title), ui.t('policy.and')) })}
72
+ {#if grace}{' '}{ui.t('policy.bannerGrace', { date: formatPolicyDate(grace) })}{/if}
73
+ </span>
74
+ <button type="button" class={ui.classes.link} on:click={begin}>{ui.t('policy.bannerReview')}</button>
75
+ </div>
76
+ {/if}
77
+ {#if controller}
78
+ <div class={classFor('card', atlas.appearance, 'atlas-card')} data-atlas-policy-gate="">
79
+ <div use:host={controller} />
80
+ {#if error}<p class={ui.classes.error} role="alert">{error}</p>{/if}
81
+ </div>
82
+ {/if}
83
+ <slot />
@@ -0,0 +1,24 @@
1
+ import { SvelteComponent } from "svelte";
2
+ declare const __propDef: {
3
+ props: {
4
+ /**
5
+ * In-app re-acceptance. For a signed-in user, a banner when a policy needs
6
+ * re-acceptance; "Review and accept" opens the unskippable dialog / checkboxes
7
+ * and posts `/v1/client/me/policies/accept`. Renders its slot always.
8
+ */ onAccepted?: (() => void) | undefined;
9
+ };
10
+ events: {
11
+ [evt: string]: CustomEvent<any>;
12
+ };
13
+ slots: {
14
+ default: {};
15
+ };
16
+ exports?: {} | undefined;
17
+ bindings?: string | undefined;
18
+ };
19
+ export type PolicyGateProps = typeof __propDef.props;
20
+ export type PolicyGateEvents = typeof __propDef.events;
21
+ export type PolicyGateSlots = typeof __propDef.slots;
22
+ export default class PolicyGate extends SvelteComponent<PolicyGateProps, PolicyGateEvents, PolicyGateSlots> {
23
+ }
24
+ export {};
@@ -59,6 +59,7 @@ $: captcha = drawn($settings, $flowState);
59
59
  enrollmentState={$enrollmentState}
60
60
  emailDelivery={$settings?.emailDelivery ?? null}
61
61
  {captcha}
62
+ onPolicyDecline={flow.declinePolicy}
62
63
  />
63
64
  {/if}
64
65
 
@@ -1,4 +1,4 @@
1
- <script>import { onDestroy } from "svelte";
1
+ <script>import { onDestroy, onMount } from "svelte";
2
2
  import { awaitingPasskey, passkeysSupported } from "@atlasauth/js";
3
3
  import { signUp } from "../accessors";
4
4
  import AttemptFlow from "./AttemptFlow.svelte";
@@ -9,6 +9,9 @@ const flow = signUp();
9
9
  const flowState = flow.state;
10
10
  const settings = flow.settings;
11
11
  onDestroy(() => flow.destroy());
12
+ onMount(() => {
13
+ void flow.loadPolicies();
14
+ });
12
15
  const drawn = (..._deps) => flow.signUpCaptcha.needed ? flow.signUpCaptcha : null;
13
16
  $: captcha = drawn($settings, $flowState);
14
17
  let mode = "password";
@@ -39,6 +42,7 @@ function switchMode() {
39
42
  busy={$flowState.busy}
40
43
  errors={$flowState.errors}
41
44
  {captcha}
45
+ policies={flow.policies}
42
46
  onSubmit={usingPasskey ? flow.createWithPasskey : flow.create}
43
47
  passkey={usingPasskey && offer ? { email: offer.email } : null}
44
48
  onSwitchMode={canPasskey ? switchMode : undefined}
@@ -1,10 +1,15 @@
1
1
  <script>import {
2
- localizedErrorMessage
2
+ confirmPolicies,
3
+ localizedErrorMessage,
4
+ policyTranslator
3
5
  } from "@atlasauth/js";
6
+ import { onDestroy } from "svelte";
4
7
  import { useAtlas } from "../context";
5
8
  import { classFor } from "../appearance";
6
9
  import { translate } from "../i18n";
7
10
  import CaptchaSlot from "./CaptchaSlot.svelte";
11
+ import PolicyClickwrap from "./PolicyClickwrap.svelte";
12
+ import PolicyConsent from "./PolicyConsent.svelte";
8
13
  export let settings = null;
9
14
  export let busy = false;
10
15
  export let errors = [];
@@ -12,15 +17,30 @@ export let captcha = null;
12
17
  export let onSubmit;
13
18
  export let passkey = null;
14
19
  export let onSwitchMode = void 0;
20
+ export let policies = null;
15
21
  const atlas = useAtlas();
16
22
  const cls = (key, base) => classFor(key, atlas.appearance, base);
17
23
  const t = (key, vars) => translate(atlas.catalog, key, vars);
18
24
  const errorText = (error) => localizedErrorMessage(error, atlas.errorCatalog ?? atlas.catalog, atlas.catalog);
19
25
  let values = {};
20
26
  let agreed = false;
21
- $: legal = settings?.legal;
27
+ let declined = null;
28
+ let policyTick = 0;
29
+ let watched = null;
30
+ let unwatch = null;
31
+ $: if (policies !== watched) {
32
+ unwatch?.();
33
+ watched = policies;
34
+ unwatch = policies ? policies.subscribe(() => policyTick += 1) : null;
35
+ }
36
+ onDestroy(() => unwatch?.());
37
+ const hasAny = (_tick, c) => !!c && c.policies.length > 0;
38
+ const okFor = (_tick, c) => !c || c.policies.length === 0 || c.canSubmit();
39
+ $: hasPolicies = hasAny(policyTick, policies);
40
+ $: legal = hasPolicies ? void 0 : settings?.legal;
22
41
  $: showLegal = !!legal && (!!legal.termsUrl || !!legal.privacyUrl);
23
- $: consentOk = !legal?.required || agreed;
42
+ $: policyOk = okFor(policyTick, policies);
43
+ $: consentOk = (!legal?.required || agreed) && policyOk;
24
44
  $: fields = settings?.signUpFields ?? [];
25
45
  $: emailUnavailable = settings?.emailDelivery?.passwordSignUp === false && !(passkey && passkey.email !== "required");
26
46
  $: showEmail = !passkey || passkey.email !== "off";
@@ -38,6 +58,14 @@ function toggle(f, option, on) {
38
58
  function handleSubmit(event) {
39
59
  event.preventDefault();
40
60
  if (!consentOk) return;
61
+ declined = null;
62
+ if (hasPolicies && policies) {
63
+ void confirmPolicies(policies, { t: policyTranslator(t) }).then((res) => {
64
+ if (res.ok) onSubmit({ ...values }, true);
65
+ else declined = res.declined;
66
+ });
67
+ return;
68
+ }
41
69
  onSubmit({ ...values }, legal?.required ? agreed : true);
42
70
  }
43
71
  </script>
@@ -188,6 +216,15 @@ function handleSubmit(event) {
188
216
  </label>
189
217
  {/if}
190
218
 
219
+ {#if hasPolicies && policies}
220
+ <PolicyConsent controller={policies} />
221
+ {#if declined}
222
+ <span class={cls('formFieldError', 'atlas-error')} role="alert" data-atlas-policy-declined="">
223
+ {t('policy.declined', { policy: declined })}
224
+ </span>
225
+ {/if}
226
+ {/if}
227
+
191
228
  {#if captcha}
192
229
  <CaptchaSlot controller={captcha} />
193
230
  {/if}
@@ -199,6 +236,9 @@ function handleSubmit(event) {
199
236
  >
200
237
  {t('signUp.submit')}
201
238
  </button>
239
+ {#if hasPolicies && policies}
240
+ <PolicyClickwrap controller={policies} />
241
+ {/if}
202
242
  {#if onSwitchMode}
203
243
  <div class="atlas-alternatives">
204
244
  <button
@@ -1,5 +1,5 @@
1
1
  import { SvelteComponent } from "svelte";
2
- import { type CaptchaController, type FieldError, type InstanceSettings } from '@atlasauth/js';
2
+ import { type CaptchaController, type FieldError, type InstanceSettings, type PolicyAcceptanceController } from '@atlasauth/js';
3
3
  declare const __propDef: {
4
4
  props: {
5
5
  /**
@@ -20,6 +20,7 @@ declare const __propDef: {
20
20
  email: "off" | "optional" | "required";
21
21
  } | null;
22
22
  /** Toggle between the password and passkey forms (only when both are offered). */ onSwitchMode?: (() => void) | undefined;
23
+ /** The instance's policies (terms, privacy, …); they replace the legacy consent row. */ policies?: PolicyAcceptanceController | null;
23
24
  };
24
25
  events: {
25
26
  [evt: string]: CustomEvent<any>;
package/dist/context.d.ts CHANGED
@@ -95,6 +95,7 @@ export interface AtlasClient extends AtlasStores {
95
95
  pendingAttempt: {
96
96
  id: string;
97
97
  status: string;
98
+ policyToken?: string;
98
99
  } | null;
99
100
  /** The session JWT; with `{ template }` a short-lived named-template token (e.g. `'chatly'`). */
100
101
  getToken(options?: {
package/dist/context.js CHANGED
@@ -52,7 +52,7 @@ export function createAtlasClient(options) {
52
52
  return null;
53
53
  const result = readRedirectResult(window.location.search);
54
54
  return result?.status && !result.ticket
55
- ? { id: result.attemptId, status: result.status }
55
+ ? { id: result.attemptId, status: result.status, ...(result.policyToken ? { policyToken: result.policyToken } : {}) }
56
56
  : null;
57
57
  })();
58
58
  async function call(path, init) {
package/dist/i18n.js CHANGED
@@ -9,7 +9,7 @@
9
9
  * A missing key renders the KEY, not an empty string. A blank button is a bug
10
10
  * nobody can report; a button reading `signIn.submit` is one anyone can.
11
11
  */
12
- import { EMAIL_UNAVAILABLE_MESSAGE } from '@atlasauth/js';
12
+ import { EMAIL_UNAVAILABLE_MESSAGE, POLICY_STRINGS } from '@atlasauth/js';
13
13
  export const EN_US = {
14
14
  'signIn.title': 'Sign in',
15
15
  'signIn.subtitle': 'to continue to {application}',
@@ -148,6 +148,8 @@ export const EN_US = {
148
148
  'impersonation.banner': 'You are signed in as {name} on someone else\'s behalf.',
149
149
  'impersonation.returnToAdmin': 'Return to admin',
150
150
  'impersonation.end': 'End impersonation',
151
+ // Policy acceptance (terms, privacy, any policy).
152
+ ...POLICY_STRINGS,
151
153
  'error.generic': 'Something went wrong. Please try again.',
152
154
  'error.network': 'We could not reach the server. Check your connection.',
153
155
  // Server errors, by the stable `reason` the Frontend API sends. Shown instead
package/dist/index.d.ts CHANGED
@@ -32,6 +32,10 @@ export { default as OrganizationProfile } from './components/OrganizationProfile
32
32
  export { default as OrganizationAuditLog } from './components/OrganizationAuditLog.svelte';
33
33
  export type { OrganizationAuditLogEntry, OrganizationAuditLogState } from '@atlasauth/js';
34
34
  export { default as ImpersonationBanner } from './components/ImpersonationBanner.svelte';
35
+ export { default as PolicyConsent } from './components/PolicyConsent.svelte';
36
+ export { default as PolicyClickwrap } from './components/PolicyClickwrap.svelte';
37
+ export { default as PolicyAcceptStep } from './components/PolicyAcceptStep.svelte';
38
+ export { default as PolicyGate } from './components/PolicyGate.svelte';
35
39
  export type { InstanceSettings, EmailDelivery, PublicSignUpField, CaptchaController, CaptchaFlowKey, CaptchaWidgetMode, FlowCaptcha, } from '@atlasauth/js';
36
40
  export { SignOutError } from '@atlasauth/js';
37
41
  export type { SignOutOptions, SignOutResult } from '@atlasauth/js';
package/dist/index.js CHANGED
@@ -35,6 +35,11 @@ export { default as OrganizationSwitcher } from './components/OrganizationSwitch
35
35
  export { default as OrganizationProfile } from './components/OrganizationProfile.svelte';
36
36
  export { default as OrganizationAuditLog } from './components/OrganizationAuditLog.svelte';
37
37
  export { default as ImpersonationBanner } from './components/ImpersonationBanner.svelte';
38
+ // Policy acceptance (terms, privacy, any policy): the consent block, clickwrap line, sign-in step and in-app gate.
39
+ export { default as PolicyConsent } from './components/PolicyConsent.svelte';
40
+ export { default as PolicyClickwrap } from './components/PolicyClickwrap.svelte';
41
+ export { default as PolicyAcceptStep } from './components/PolicyAcceptStep.svelte';
42
+ export { default as PolicyGate } from './components/PolicyGate.svelte';
38
43
  // What `signOut()` resolves with, and what it rejects with under `{ strict: true }`.
39
44
  export { SignOutError } from '@atlasauth/js';
40
45
  // `onSignOut({ reason })` event types + the `atlas:signout` window event; this SDK's version.
@@ -0,0 +1,6 @@
1
+ import { type PolicyClasses, type PolicyT } from '@atlasauth/js';
2
+ /** The policy UI's translator (catalog, else the English policy strings) and class hooks, for the current Atlas context. */
3
+ export declare function usePolicyUi(): {
4
+ t: PolicyT;
5
+ classes: PolicyClasses;
6
+ };
@@ -0,0 +1,17 @@
1
+ import { policyTranslator } from '@atlasauth/js';
2
+ import { useAtlas } from './context';
3
+ import { classFor } from './appearance';
4
+ import { translate } from './i18n';
5
+ /** The policy UI's translator (catalog, else the English policy strings) and class hooks, for the current Atlas context. */
6
+ export function usePolicyUi() {
7
+ const atlas = useAtlas();
8
+ return {
9
+ t: policyTranslator((key, vars) => translate(atlas.catalog, key, vars)),
10
+ classes: {
11
+ link: classFor('link', atlas.appearance, 'atlas-link'),
12
+ text: classFor('text', atlas.appearance, 'atlas-text'),
13
+ primaryButton: classFor('formButtonPrimary', atlas.appearance, 'atlas-button'),
14
+ error: classFor('formFieldError', atlas.appearance, 'atlas-error'),
15
+ },
16
+ };
17
+ }
package/dist/version.d.ts CHANGED
@@ -1,4 +1,4 @@
1
1
  import type { AtlasSdkInfo } from '@atlasauth/js';
2
2
  /** This package's version, sent as `X-Atlas-SDK` (kept in step with package.json by a test). */
3
- export declare const ATLAS_SVELTE_VERSION = "0.9.0";
3
+ export declare const ATLAS_SVELTE_VERSION = "0.10.0";
4
4
  export declare const ATLAS_SVELTE_SDK: AtlasSdkInfo;
package/dist/version.js CHANGED
@@ -1,3 +1,3 @@
1
1
  /** This package's version, sent as `X-Atlas-SDK` (kept in step with package.json by a test). */
2
- export const ATLAS_SVELTE_VERSION = '0.9.0';
2
+ export const ATLAS_SVELTE_VERSION = '0.10.0';
3
3
  export const ATLAS_SVELTE_SDK = { name: '@atlasauth/svelte', version: ATLAS_SVELTE_VERSION };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atlasauth/svelte",
3
- "version": "0.9.0",
3
+ "version": "0.11.0",
4
4
  "type": "module",
5
5
  "svelte": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -22,10 +22,10 @@
22
22
  "./package.json": "./package.json"
23
23
  },
24
24
  "dependencies": {
25
- "@atlasauth/design": "0.2.0",
26
- "@atlasauth/authz": "0.2.0",
27
- "@atlasauth/backend": "0.15.0",
28
- "@atlasauth/js": "0.16.0"
25
+ "@atlasauth/design": "0.3.0",
26
+ "@atlasauth/backend": "0.19.0",
27
+ "@atlasauth/js": "0.20.0",
28
+ "@atlasauth/authz": "0.2.0"
29
29
  },
30
30
  "peerDependencies": {
31
31
  "svelte": ">=4",