@atlasauth/svelte 0.6.1 → 0.8.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,20 @@
1
1
  # @atlasauth/svelte changelog
2
2
 
3
+ ## 0.8.0
4
+
5
+ ### Changed
6
+
7
+ - `@atlasauth/js` 0.15.0: referral capture for any referral program and any consent manager (pass a custom program's reader page-wide with `configureReferralCapture({ referralSource })`).
8
+
9
+ ### Fixed
10
+
11
+ - Development instances: sign-in from `localhost` keeps working where the browser blocks the instance's cookie as third-party (Chrome, Safari). On a `pk_test_` key the SDK keeps the session's refresh token in `localStorage` and sends it as `x-refresh-token`; production keys never do.
12
+ - After a sign-in completes, the panel says "You're signed in" only once this browser holds the session, and says plainly when the browser didn't keep it, instead of "you are signed in where you started". That message is now shown only for a sign-in completed on another device.
13
+ - `<SignIn>` shows "No account? Sign up" on every step (`signUpUrl`; default the hosted sign-up page with the visitor's referral, `null` hides it), so a newcomer on a password-only instance always has a way to create an account.
14
+
15
+ ## 0.7.0
16
+ - **Referral attribution.** The prebuilt forms send the referral the visitor arrived with on sign-in and sign-up (through `@atlasauth/js` 0.13.0) when the instance has referrals on, and show the captcha for a referred sign-up when the instance requires it. New init options: `referral?: false` and `referralConsent?`.
17
+
3
18
  ## 0.6.1
4
19
  - **`<UserProfile>` can verify an address.** Each unverified email address now has a **Verify** button: it emails a code (`POST /v1/client/me/email_addresses/{id}/prepare_verification`) and shows a field to enter it (`attempt_verification`), then reloads the user. This includes the sign-up address on an app that does not require email verification, which now reads unverified until it is proven. New catalog keys: `userProfile.verify`, `userProfile.verifyConfirm`, `userProfile.verifyCodePlaceholder`, `userProfile.verifySendFailed`, `userProfile.verifyCodeRejected`.
5
20
 
package/dist/accessors.js CHANGED
@@ -115,11 +115,12 @@ function createFlow() {
115
115
  return;
116
116
  // The ticket is exchanged by a normal request, so no session token has
117
117
  // ever appeared in a URL.
118
- await client.post('/v1/client/tickets/exchange', {
118
+ const res = await client.post('/v1/client/tickets/exchange', {
119
119
  attempt_id: next.attempt.id,
120
120
  ticket,
121
121
  });
122
- await atlas.reload();
122
+ const kept = res.ok && (await atlas.reload());
123
+ state.update((current) => ({ ...current, sessionKept: kept }));
123
124
  })();
124
125
  }, 2_000);
125
126
  }
@@ -127,18 +128,24 @@ function createFlow() {
127
128
  // for cookies over a same-origin request — FAPI is cross-origin to the app
128
129
  // and cannot set the cookie on the completion response itself — THEN
129
130
  // re-boot so the rest of the app sees a signed-in user.
131
+ // Stays busy until this browser actually holds the session: the completion
132
+ // alone says nothing about whether the cookie was kept.
130
133
  async function finish(next) {
131
- state.set(next);
132
134
  if (next.attempt?.status === 'complete') {
135
+ state.set({ ...next, busy: true });
136
+ let exchanged = true;
133
137
  if (next.ticket) {
134
- await client.post('/v1/client/tickets/exchange', {
138
+ const res = await client.post('/v1/client/tickets/exchange', {
135
139
  attempt_id: next.attempt.id,
136
140
  ticket: next.ticket,
137
141
  });
142
+ exchanged = res.ok;
138
143
  }
139
- await atlas.reload();
144
+ const kept = exchanged && (await atlas.reload());
145
+ state.set({ ...next, busy: false, sessionKept: kept });
140
146
  return;
141
147
  }
148
+ state.set(next);
142
149
  maybeStartPolling();
143
150
  }
144
151
  function submit(values) {
@@ -11,6 +11,7 @@ import CaptchaSlot from "./CaptchaSlot.svelte";
11
11
  export let attempt = null;
12
12
  export let busy = false;
13
13
  export let errors = [];
14
+ export let sessionKept = void 0;
14
15
  export let titleKey;
15
16
  export let onSubmit;
16
17
  export let captcha = null;
@@ -75,6 +76,8 @@ $: message = (() => {
75
76
  case "await_oauth":
76
77
  return t("signIn.magicLinkSent");
77
78
  case "done":
79
+ if (sessionKept === true) return t("signIn.signedIn");
80
+ if (sessionKept === false) return t("signIn.sessionNotKept");
78
81
  return t("signIn.checkOtherTab");
79
82
  case "restart":
80
83
  return t("error.generic");
@@ -114,7 +117,11 @@ function handleSubmit(event) {
114
117
 
115
118
  <form on:submit={handleSubmit}>
116
119
  {#if message !== null}
117
- <p>{message}</p>
120
+ {#if step.kind === 'done' && sessionKept === false}
121
+ <p class={cls('formFieldError', 'atlas-error')} role="alert">{message}</p>
122
+ {:else}
123
+ <p>{message}</p>
124
+ {/if}
118
125
  {:else}
119
126
  {#if step.kind === 'collect_captcha'}
120
127
  <!-- §5 the server withheld the attempt until a captcha clears. Solving
@@ -12,6 +12,7 @@ declare const __propDef: {
12
12
  */ attempt?: AttemptView | null;
13
13
  busy?: boolean;
14
14
  errors?: FieldError[];
15
+ /** `FlowState.sessionKept`: whether this browser kept the session the sign-in created. */ sessionKept?: boolean | undefined;
15
16
  titleKey: string;
16
17
  onSubmit: (values: Record<string, string>) => void;
17
18
  /**
@@ -1,7 +1,28 @@
1
1
  <script>import { onDestroy } from "svelte";
2
+ import { signUp as hostedSignUp } from "@atlasauth/js";
2
3
  import { signIn } from "../accessors";
4
+ import { useAtlas } from "../context";
5
+ import { classFor } from "../appearance";
6
+ import { translate } from "../i18n";
3
7
  import AttemptFlow from "./AttemptFlow.svelte";
4
8
  export let afterUrl = void 0;
9
+ export let signUpUrl = void 0;
10
+ const atlas = useAtlas();
11
+ const t = (key) => translate(atlas.catalog, key);
12
+ function signUpHref(url, after) {
13
+ if (url === null) return null;
14
+ if (url) return url;
15
+ try {
16
+ return hostedSignUp({
17
+ frontendApi: atlas.frontendApi,
18
+ redirectUrl: after ?? (typeof window !== "undefined" ? window.location.href : void 0),
19
+ navigate: false
20
+ });
21
+ } catch {
22
+ return null;
23
+ }
24
+ }
25
+ $: promptHref = $flowState.attempt?.status !== "complete" ? signUpHref(signUpUrl, afterUrl) : null;
5
26
  const flow = signIn();
6
27
  const flowState = flow.state;
7
28
  const settings = flow.settings;
@@ -14,9 +35,16 @@ $: captcha = drawn($settings, $flowState);
14
35
  attempt={$flowState.attempt}
15
36
  busy={$flowState.busy}
16
37
  errors={$flowState.errors}
38
+ sessionKept={$flowState.sessionKept}
17
39
  titleKey="signIn.title"
18
40
  onSubmit={flow.submit}
19
41
  onPrepare={flow.prepare}
20
42
  emailDelivery={$settings?.emailDelivery ?? null}
21
43
  {captcha}
22
44
  />
45
+
46
+ {#if promptHref}
47
+ <p class={classFor('footerAction', atlas.appearance, 'atlas-footer')} data-atlas-sign-up-prompt="">
48
+ {t('signIn.noAccount')} <a class="atlas-link" href={promptHref}>{t('signIn.signUpLink')}</a>
49
+ </p>
50
+ {/if}
@@ -9,6 +9,11 @@ declare const __propDef: {
9
9
  * Offers emailed first factors (code / link) only while the app can deliver
10
10
  * email (`settings.emailDelivery`).
11
11
  */ afterUrl?: string | undefined;
12
+ /**
13
+ * Where "No account? Sign up" goes. Default: the instance's hosted sign-up
14
+ * page (carrying the visitor's referral). `null` hides it. Always shown, never
15
+ * only after a failed sign-in, so it says nothing about whether an email has an account.
16
+ */ signUpUrl?: string | null | undefined;
12
17
  };
13
18
  events: {
14
19
  [evt: string]: CustomEvent<any>;
@@ -16,6 +16,7 @@ $: captcha = drawn($settings, $flowState);
16
16
  attempt={$flowState.attempt}
17
17
  busy={$flowState.busy}
18
18
  errors={$flowState.errors}
19
+ sessionKept={$flowState.sessionKept}
19
20
  titleKey="signUp.title"
20
21
  onSubmit={flow.submit}
21
22
  />
package/dist/context.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { type Readable } from 'svelte/store';
2
- import { type ProviderToken, type SignOutOptions, type SignOutResult, type SignOutEvent, type SignOutListener } from '@atlasauth/js';
2
+ import { type ProviderToken, type SignOutOptions, type SignOutResult, type SignOutEvent, type SignOutListener, type ReferralConsent } from '@atlasauth/js';
3
3
  import { type Catalog } from './i18n';
4
4
  import { type Appearance, type InstanceDesign } from './appearance';
5
5
  import type { AtlasOrganizationMembership, AtlasSession, AtlasUser, SessionClaims } from './types';
@@ -23,6 +23,18 @@ export type AuthStatus = 'loading' | 'signed_in' | 'signed_out';
23
23
  export declare const ATLAS_KEY: unique symbol;
24
24
  export interface AtlasInitOptions {
25
25
  publishableKey: string;
26
+ /**
27
+ * Referral attribution: the prebuilt forms send the referral the visitor
28
+ * arrived with on sign-in / sign-up when the instance has referrals on.
29
+ * `false` turns that off.
30
+ */
31
+ referral?: false;
32
+ /**
33
+ * The app's own consent decision for the referral tracking cookie, used only
34
+ * when Cookie Munch is not on the page: `'granted'` (or a function returning
35
+ * it) lets the forms read the cookie. Default: the cookie is not read.
36
+ */
37
+ referralConsent?: ReferralConsent;
26
38
  /**
27
39
  * The instance's Frontend API: an absolute URL such as
28
40
  * `"https://auth.example.com"`, or `"same-origin"` / a path such as `"/atlas"`
@@ -114,7 +126,8 @@ export interface AtlasClient extends AtlasStores {
114
126
  */
115
127
  signOut(options?: SignOutOptions): Promise<SignOutResult>;
116
128
  setActiveOrganization(organizationId: string | null): Promise<void>;
117
- reload(): Promise<void>;
129
+ /** Re-boot from the Frontend API. Resolves true when a session now exists in this browser. */
130
+ reload(): Promise<boolean>;
118
131
  /**
119
132
  * Redirect handling + the first `/v1/client` load. Runs once, in the browser
120
133
  * only. {@link initAtlas} calls it automatically unless `manualBoot` is set;
package/dist/context.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { getContext, setContext } from 'svelte';
2
2
  import { derived, get, writable } from 'svelte/store';
3
- import { FapiClient, SignOutGuard, TokenCache, closeOAuthPopupAfterSignIn, completeOAuthPopupLanding, getProviderToken as fetchProviderToken, mapOAuthPopupPayload, msUntilRefresh, readExpiry, readRedirectResult, resolveFrontendApi, stripRedirectParams, catalogLocale, SDK_HEADER, SignOutNotifier, getTemplateToken, reportSdkAdvisory, sdkHeaderValue, signOutReasonFromClient, } from '@atlasauth/js';
3
+ import { FapiClient, SignOutGuard, TokenCache, closeOAuthPopupAfterSignIn, completeOAuthPopupLanding, getProviderToken as fetchProviderToken, mapOAuthPopupPayload, msUntilRefresh, readExpiry, readRedirectResult, resolveFrontendApi, stripRedirectParams, catalogLocale, SDK_HEADER, SignOutNotifier, getTemplateToken, reportSdkAdvisory, sdkHeaderValue, signOutReasonFromClient, configureReferralCapture, withDevBrowser, } from '@atlasauth/js';
4
4
  import { ATLAS_SVELTE_SDK } from './version';
5
5
  import { EN_US, withFallback } from './i18n';
6
6
  import { instanceDesignFrom } from './appearance';
@@ -28,7 +28,9 @@ export function createAtlasClient(options) {
28
28
  const baseUrl = resolveFrontendApi(options.frontendApi, {
29
29
  component: '<AtlasProvider> / initAtlas()',
30
30
  });
31
- const doFetch = options.fetchImpl ?? (typeof fetch !== 'undefined' ? fetch : undefined);
31
+ const rawFetch = options.fetchImpl ?? (typeof fetch !== 'undefined' ? fetch : undefined);
32
+ // Development instances keep the session in a header where the browser blocks the cookie.
33
+ const doFetch = rawFetch ? withDevBrowser(options.publishableKey, rawFetch) : undefined;
32
34
  const catalog = options.localization ? withFallback(options.localization) : EN_US;
33
35
  const errorCatalog = options.localization ?? EN_US;
34
36
  const locale = catalogLocale(options.localization);
@@ -139,7 +141,7 @@ export function createAtlasClient(options) {
139
141
  // refused: stay signed out; the guard re-tries the revoke in the background.
140
142
  if (!body.session || !body.user || guard.refuse(body.session.id)) {
141
143
  clearSession(body.session ? 'other_tab' : signOutReasonFromClient(body));
142
- return;
144
+ return false;
143
145
  }
144
146
  notifier.clear();
145
147
  user.set(body.user);
@@ -149,6 +151,7 @@ export function createAtlasClient(options) {
149
151
  applyToken(body.jwt, body.session.id);
150
152
  status.set('signed_in');
151
153
  scheduleRefresh();
154
+ return true;
152
155
  }
153
156
  catch {
154
157
  /**
@@ -157,6 +160,7 @@ export function createAtlasClient(options) {
157
160
  * re-authenticate for no reason.
158
161
  */
159
162
  status.update((current) => (current === 'loading' ? 'signed_out' : current));
163
+ return false;
160
164
  }
161
165
  }
162
166
  async function refresh() {
@@ -191,6 +195,8 @@ export function createAtlasClient(options) {
191
195
  await refresh();
192
196
  return cache.get(5_000)?.jwt ?? null;
193
197
  }
198
+ // Referral attribution: app-wide options every FAPI client on the page falls back to.
199
+ configureReferralCapture({ referral: options.referral, referralConsent: options.referralConsent });
194
200
  const fapiClient = new FapiClient({ publishableKey, baseUrl, fetchImpl: doFetch, locale, sdk: ATLAS_SVELTE_SDK });
195
201
  function getProviderToken(provider) {
196
202
  return fetchProviderToken(fapiClient, provider);
package/dist/i18n.js CHANGED
@@ -31,6 +31,8 @@ export const EN_US = {
31
31
  'signIn.resendCode': 'Resend code',
32
32
  'signIn.resendLink': 'Resend link',
33
33
  'signIn.passwordOption': 'Use your password instead',
34
+ 'signIn.signedIn': 'You\'re signed in.',
35
+ 'signIn.sessionNotKept': 'You signed in, but this browser didn\'t keep the session. If it blocks third-party cookies, allow cookies for this site and try again.',
34
36
  'signIn.checkOtherTab': 'You can close this tab — you are signed in where you started.',
35
37
  'signUp.title': 'Create your account',
36
38
  'signUp.submit': 'Continue',
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.6.1";
3
+ export declare const ATLAS_SVELTE_VERSION = "0.8.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.6.1';
2
+ export const ATLAS_SVELTE_VERSION = '0.8.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.6.1",
3
+ "version": "0.8.0",
4
4
  "type": "module",
5
5
  "svelte": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -23,9 +23,9 @@
23
23
  },
24
24
  "dependencies": {
25
25
  "@atlasauth/design": "0.2.0",
26
- "@atlasauth/js": "0.12.0",
26
+ "@atlasauth/backend": "0.14.0",
27
27
  "@atlasauth/authz": "0.1.1",
28
- "@atlasauth/backend": "0.9.0"
28
+ "@atlasauth/js": "0.15.0"
29
29
  },
30
30
  "peerDependencies": {
31
31
  "svelte": ">=4",