@dloizides/auth-web 1.4.0 → 1.5.1

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/dist/index.mjs CHANGED
@@ -139,6 +139,21 @@ var DEFAULT_PASSKEY_SETTINGS_LABELS = {
139
139
  addHint: "Start setting up a passkey for this account",
140
140
  registeredSuccess: "Your passkey was added. You can now use it to sign in."
141
141
  };
142
+ var DEFAULT_PREFERRED_METHOD_SETTINGS_LABELS = {
143
+ title: "Preferred sign-in method",
144
+ description: "Pick how you usually sign in. We will show this method first on your devices.",
145
+ noPreference: "No preference",
146
+ methodPassword: "Password",
147
+ methodPin: "Quick PIN",
148
+ methodPasskey: "Passkey",
149
+ saving: "Saving...",
150
+ saveFailed: "Could not save your preference. Try again later."
151
+ };
152
+ var DEFAULT_PREFERRED_METHOD_HINT_LABELS = {
153
+ password: "You usually sign in with your password.",
154
+ pin: "You usually sign in with your quick PIN on this account.",
155
+ passkey: "You usually sign in with a passkey."
156
+ };
142
157
  var DEFAULT_RESET_PASSWORD_LABELS = {
143
158
  title: "Reset password",
144
159
  description: "Choose a new password for your account.",
@@ -219,7 +234,14 @@ var AuthTestIds = {
219
234
  // Passkey settings card
220
235
  passkeySettings: "auth-passkey-settings",
221
236
  passkeySettingsAdd: "auth-passkey-settings-add",
222
- passkeySettingsSuccess: "auth-passkey-settings-success"
237
+ passkeySettingsSuccess: "auth-passkey-settings-success",
238
+ // Preferred-method settings card (cross-device default-method picker).
239
+ // Each option row appends `-${method}` to `preferredMethodSettingsOption`.
240
+ preferredMethodSettings: "auth-preferred-method-settings",
241
+ preferredMethodSettingsOption: "auth-preferred-method-settings-option",
242
+ preferredMethodSettingsError: "auth-preferred-method-settings-error",
243
+ // Preferred-method login-screen hint
244
+ preferredMethodHint: "auth-preferred-method-hint"
223
245
  };
224
246
  function withTestIdPrefix(baseId, prefix) {
225
247
  return prefix === void 0 || prefix === "" ? baseId : `${prefix}-${baseId}`;
@@ -282,6 +304,11 @@ function useAuthStyles(theme) {
282
304
  primaryButton: {
283
305
  borderRadius: radii.input,
284
306
  paddingVertical: BUTTON_VERTICAL_PADDING,
307
+ paddingHorizontal: spacing.md,
308
+ // Never shrink below the label width: in a row of actions (e.g. the
309
+ // forgot-password modal's Cancel + submit), shrinking would clip a long
310
+ // label like "Send reset link" past the button's rounded edge.
311
+ flexShrink: 0,
285
312
  alignItems: "center",
286
313
  justifyContent: "center",
287
314
  marginTop: spacing.xs,
@@ -324,6 +351,8 @@ function useAuthStyles(theme) {
324
351
  borderWidth: 1,
325
352
  borderColor: colors.primary,
326
353
  paddingVertical: BUTTON_VERTICAL_PADDING,
354
+ paddingHorizontal: spacing.md,
355
+ flexShrink: 0,
327
356
  alignItems: "center",
328
357
  justifyContent: "center",
329
358
  marginTop: spacing.sm,
@@ -800,6 +829,9 @@ function useModalStyles(theme) {
800
829
  flexDirection: "row",
801
830
  justifyContent: "flex-end",
802
831
  alignItems: "center",
832
+ // Wrap (rather than clip) when a long submit label + Cancel can't
833
+ // fit a narrow modal on one line.
834
+ flexWrap: "wrap",
803
835
  marginTop: theme.spacing.md,
804
836
  gap: theme.spacing.sm
805
837
  },
@@ -807,6 +839,8 @@ function useModalStyles(theme) {
807
839
  borderRadius: theme.radii.input,
808
840
  paddingVertical: theme.spacing.sm,
809
841
  paddingHorizontal: theme.spacing.md,
842
+ // Match the submit button: never shrink below the label width.
843
+ flexShrink: 0,
810
844
  borderWidth: SECONDARY_BORDER_WIDTH,
811
845
  borderColor: theme.colors.border
812
846
  },
@@ -2526,6 +2560,217 @@ function PasskeySettingsCard({
2526
2560
  }
2527
2561
  );
2528
2562
  }
2563
+
2564
+ // src/preferences/preferredMethodCache.ts
2565
+ var CACHE_KEY = "dloizides.auth.preferredLoginMethod";
2566
+ function safeStorage() {
2567
+ try {
2568
+ if (typeof localStorage === "undefined") {
2569
+ return null;
2570
+ }
2571
+ return localStorage;
2572
+ } catch {
2573
+ return null;
2574
+ }
2575
+ }
2576
+ function readCachedPreferredMethod() {
2577
+ const store = safeStorage();
2578
+ if (store === null) {
2579
+ return null;
2580
+ }
2581
+ try {
2582
+ const value = store.getItem(CACHE_KEY);
2583
+ return value === null || value === "" ? null : value;
2584
+ } catch {
2585
+ return null;
2586
+ }
2587
+ }
2588
+ function cachePreferredMethod(method) {
2589
+ const store = safeStorage();
2590
+ if (store === null) {
2591
+ return;
2592
+ }
2593
+ try {
2594
+ if (method === null || method === "") {
2595
+ store.removeItem(CACHE_KEY);
2596
+ return;
2597
+ }
2598
+ store.setItem(CACHE_KEY, method);
2599
+ } catch {
2600
+ }
2601
+ }
2602
+ function clearCachedPreferredMethod() {
2603
+ cachePreferredMethod(null);
2604
+ }
2605
+
2606
+ // src/preferences/usePreferredMethodSetting.ts
2607
+ function usePreferredMethodSetting({
2608
+ client,
2609
+ onChanged
2610
+ }) {
2611
+ const [loading, setLoading] = useState(true);
2612
+ const [saving, setSaving] = useState(false);
2613
+ const [failed, setFailed] = useState(false);
2614
+ const [selected, setSelected] = useState(null);
2615
+ useEffect(() => {
2616
+ let active = true;
2617
+ client.getPreferredLoginMethod().then((method) => {
2618
+ if (active) {
2619
+ setSelected(method);
2620
+ }
2621
+ }).catch(() => {
2622
+ }).finally(() => {
2623
+ if (active) {
2624
+ setLoading(false);
2625
+ }
2626
+ });
2627
+ return () => {
2628
+ active = false;
2629
+ };
2630
+ }, [client]);
2631
+ const select = useCallback(
2632
+ (method) => {
2633
+ const previous = selected;
2634
+ if (method === previous) {
2635
+ return;
2636
+ }
2637
+ setFailed(false);
2638
+ setSaving(true);
2639
+ setSelected(method);
2640
+ client.setPreferredLoginMethod(method).then((ok) => {
2641
+ if (ok) {
2642
+ cachePreferredMethod(method);
2643
+ onChanged?.(method);
2644
+ return;
2645
+ }
2646
+ setSelected(previous);
2647
+ setFailed(true);
2648
+ }).catch(() => {
2649
+ setSelected(previous);
2650
+ setFailed(true);
2651
+ }).finally(() => {
2652
+ setSaving(false);
2653
+ });
2654
+ },
2655
+ [client, selected, onChanged]
2656
+ );
2657
+ return { loading, saving, failed, selected, select };
2658
+ }
2659
+ var NO_PREFERENCE = "__none__";
2660
+ function methodDisplayName(method, labels) {
2661
+ if (method === "password") {
2662
+ return labels.methodPassword;
2663
+ }
2664
+ if (method === "pin") {
2665
+ return labels.methodPin;
2666
+ }
2667
+ if (method === "passkey") {
2668
+ return labels.methodPasskey;
2669
+ }
2670
+ return method;
2671
+ }
2672
+ function PreferredMethodSettingsCard({
2673
+ client,
2674
+ availableMethods,
2675
+ theme: themeProp,
2676
+ labels: labelsProp,
2677
+ testIdPrefix,
2678
+ onChanged
2679
+ }) {
2680
+ const theme = useAuthTheme(themeProp);
2681
+ const styles3 = useAuthStyles(theme);
2682
+ const labels = useMemo(
2683
+ () => ({ ...DEFAULT_PREFERRED_METHOD_SETTINGS_LABELS, ...labelsProp }),
2684
+ [labelsProp]
2685
+ );
2686
+ const { loading, saving, failed, selected, select } = usePreferredMethodSetting({
2687
+ client,
2688
+ onChanged
2689
+ });
2690
+ const options = useMemo(
2691
+ () => [
2692
+ { value: null, key: NO_PREFERENCE, label: labels.noPreference },
2693
+ ...availableMethods.map((method) => ({
2694
+ value: method,
2695
+ key: method,
2696
+ label: methodDisplayName(method, labels)
2697
+ }))
2698
+ ],
2699
+ [availableMethods, labels]
2700
+ );
2701
+ return /* @__PURE__ */ jsxs(
2702
+ View,
2703
+ {
2704
+ style: styles3.card,
2705
+ testID: withTestIdPrefix(AuthTestIds.preferredMethodSettings, testIdPrefix),
2706
+ children: [
2707
+ /* @__PURE__ */ jsx(Text, { style: styles3.title, children: labels.title }),
2708
+ /* @__PURE__ */ jsx(Text, { style: styles3.subtitle, children: labels.description }),
2709
+ loading ? /* @__PURE__ */ jsx(ActivityIndicator, { color: theme.colors.primary, size: "small" }) : options.map((option) => {
2710
+ const isSelected = option.value === selected;
2711
+ const optionTestId = `${withTestIdPrefix(
2712
+ AuthTestIds.preferredMethodSettingsOption,
2713
+ testIdPrefix
2714
+ )}-${option.key}`;
2715
+ return /* @__PURE__ */ jsx(
2716
+ TouchableOpacity,
2717
+ {
2718
+ accessibilityHint: labels.description,
2719
+ accessibilityLabel: option.label,
2720
+ accessibilityRole: "radio",
2721
+ accessibilityState: { selected: isSelected },
2722
+ disabled: saving,
2723
+ style: isSelected ? styles3.secondaryButton : styles3.escapeLink,
2724
+ testID: optionTestId,
2725
+ onPress: () => select(option.value),
2726
+ children: /* @__PURE__ */ jsx(Text, { style: isSelected ? styles3.secondaryButtonText : styles3.linkText, children: isSelected ? `\u2713 ${option.label}` : option.label })
2727
+ },
2728
+ option.key
2729
+ );
2730
+ }),
2731
+ saving ? /* @__PURE__ */ jsx(Text, { style: styles3.helperText, children: labels.saving }) : null,
2732
+ failed ? /* @__PURE__ */ jsx(
2733
+ Text,
2734
+ {
2735
+ style: styles3.errorText,
2736
+ testID: withTestIdPrefix(AuthTestIds.preferredMethodSettingsError, testIdPrefix),
2737
+ children: labels.saveFailed
2738
+ }
2739
+ ) : null
2740
+ ]
2741
+ }
2742
+ );
2743
+ }
2744
+ function hintFor(method, labels) {
2745
+ if (method === "password") {
2746
+ return labels.password;
2747
+ }
2748
+ if (method === "pin") {
2749
+ return labels.pin;
2750
+ }
2751
+ if (method === "passkey") {
2752
+ return labels.passkey;
2753
+ }
2754
+ return null;
2755
+ }
2756
+ function PreferredMethodHint({
2757
+ method,
2758
+ theme: themeProp,
2759
+ labels: labelsProp,
2760
+ testIdPrefix
2761
+ }) {
2762
+ const theme = useAuthTheme(themeProp);
2763
+ const styles3 = useAuthStyles(theme);
2764
+ const labels = useMemo(
2765
+ () => ({ ...DEFAULT_PREFERRED_METHOD_HINT_LABELS, ...labelsProp }),
2766
+ [labelsProp]
2767
+ );
2768
+ const text = hintFor(method, labels);
2769
+ if (text === null) {
2770
+ return null;
2771
+ }
2772
+ return /* @__PURE__ */ jsx(View, { testID: withTestIdPrefix(AuthTestIds.preferredMethodHint, testIdPrefix), children: /* @__PURE__ */ jsx(Text, { style: styles3.helperText, children: text }) });
2773
+ }
2529
2774
  var EMPTY_CONFIG = {
2530
2775
  methods: [],
2531
2776
  registrationEnabled: false,
@@ -2560,6 +2805,77 @@ function useBffLoginConfig(client) {
2560
2805
  }, [client]);
2561
2806
  return { config, loading };
2562
2807
  }
2808
+
2809
+ // src/preferences/preferencesClient.ts
2810
+ var CSRF_HEADER = "X-BFF-Csrf";
2811
+ var CSRF_HEADER_VALUE = "1";
2812
+ var JSON_CONTENT_TYPE = "application/json";
2813
+ var DEFAULT_TENANTS_SEGMENT = "tenants";
2814
+ var PREFERENCES_PATH = "api/v1/me/preferences";
2815
+ var LOGIN_METHOD_PREFERENCE_PATH = "api/v1/me/login-method-preference";
2816
+ function isRecord(value) {
2817
+ return typeof value === "object" && value !== null;
2818
+ }
2819
+ function readOptionalString(record, key) {
2820
+ const value = record[key];
2821
+ if (typeof value !== "string" || value === "") {
2822
+ return null;
2823
+ }
2824
+ return value;
2825
+ }
2826
+ function createPreferencesClient(options) {
2827
+ const http = options.http;
2828
+ const baseUrl = (options.baseUrl ?? "").replace(/\/$/, "");
2829
+ const segment = options.tenantsSegment ?? DEFAULT_TENANTS_SEGMENT;
2830
+ const proxyBase = `${baseUrl}/bff/api/${segment}`;
2831
+ return {
2832
+ async getPreferredLoginMethod() {
2833
+ try {
2834
+ const response = await http({
2835
+ url: `${proxyBase}/${PREFERENCES_PATH}`,
2836
+ method: "GET",
2837
+ headers: { Accept: JSON_CONTENT_TYPE },
2838
+ credentials: "include"
2839
+ });
2840
+ if (!response.ok || !isRecord(response.data)) {
2841
+ return null;
2842
+ }
2843
+ return readOptionalString(response.data, "preferredLoginMethod");
2844
+ } catch {
2845
+ return null;
2846
+ }
2847
+ },
2848
+ async setPreferredLoginMethod(method) {
2849
+ try {
2850
+ const response = await http({
2851
+ url: `${proxyBase}/${LOGIN_METHOD_PREFERENCE_PATH}`,
2852
+ method: "PUT",
2853
+ headers: {
2854
+ "Content-Type": JSON_CONTENT_TYPE,
2855
+ Accept: JSON_CONTENT_TYPE,
2856
+ [CSRF_HEADER]: CSRF_HEADER_VALUE
2857
+ },
2858
+ body: JSON.stringify({ method }),
2859
+ credentials: "include"
2860
+ });
2861
+ return response.ok;
2862
+ } catch {
2863
+ return false;
2864
+ }
2865
+ }
2866
+ };
2867
+ }
2868
+
2869
+ // src/preferences/syncPreferredMethodCache.ts
2870
+ async function syncPreferredMethodFromServer(client) {
2871
+ try {
2872
+ const method = await client.getPreferredLoginMethod();
2873
+ cachePreferredMethod(method);
2874
+ return method;
2875
+ } catch {
2876
+ return null;
2877
+ }
2878
+ }
2563
2879
  async function lazyFetchHttpClient(request) {
2564
2880
  const fetchImpl = typeof fetch === "function" ? fetch.bind(globalThis) : void 0;
2565
2881
  if (fetchImpl === void 0) {
@@ -2575,11 +2891,11 @@ function createBffAuthClient(options = {}) {
2575
2891
  }
2576
2892
 
2577
2893
  // src/router/resolvePostLoginRoute.ts
2578
- function isRecord(value) {
2894
+ function isRecord2(value) {
2579
2895
  return typeof value === "object" && value !== null;
2580
2896
  }
2581
2897
  function readRealmAccessRoles(realmAccess) {
2582
- if (!isRecord(realmAccess)) {
2898
+ if (!isRecord2(realmAccess)) {
2583
2899
  return [];
2584
2900
  }
2585
2901
  const roles = realmAccess.roles;
@@ -2603,6 +2919,6 @@ function resolvePostLoginRoute(user, table) {
2603
2919
  return table.fallback ?? null;
2604
2920
  }
2605
2921
 
2606
- export { AuthTestIds, AuthThemeProvider, BffAuthStatus, DEFAULT_DEVICE_PIN_ENROLL_LABELS, DEFAULT_DEVICE_PIN_SETTINGS_LABELS, DEFAULT_DEVICE_PIN_UNLOCK_LABELS, DEFAULT_FORGOT_PASSWORD_FIELDS_LABELS, DEFAULT_FORGOT_PASSWORD_LABELS, DEFAULT_LOGIN_LABELS, DEFAULT_OTP_LABELS, DEFAULT_PASSKEY_LOGIN_LABELS, DEFAULT_PASSKEY_SETTINGS_LABELS, DEFAULT_PIN_LABELS, DEFAULT_RESET_PASSWORD_LABELS, DEVICE_PIN_ALLOWED_DIGITS, DEVICE_PIN_DEFAULT_DIGITS, DevicePinEnrollErrorKey, DevicePinEnrollForm, DevicePinErrorKey, DevicePinInput, DevicePinLengthPicker, DevicePinOffer, DevicePinSettingsCard, DevicePinUnlockScreen, ForgotPasswordFields, ForgotPasswordForm, LoginForm, OtpForm, OtpLoginStep, PASSWORD_MAX_LENGTH, PASSWORD_MIN_LENGTH, PasskeyLoginButton, PasskeySettingsCard, PasswordPolicyError, PinForm, ResetPasswordError, ResetPasswordForm, collectUserRoles, createBffAuthClient, defaultAuthTheme, isAllowedPinDigits, isPasswordValid, isValidForgotPasswordEmail, readPasskeyError, readPasskeyRegistered, resolvePostLoginRoute, startPasskeyLogin, startPasskeyRegistration, useAuthTheme, useBffAuth, useBffForgotPassword, useBffLoginConfig, useBffResetPassword, useDevicePinDisable, useDevicePinEnroll, useDevicePinUnlock, useForgotPasswordSubmit, useOtpLogin, usePinLogin, useResetPasswordForm, validatePasswordPolicy, withTestIdPrefix };
2922
+ export { AuthTestIds, AuthThemeProvider, BffAuthStatus, DEFAULT_DEVICE_PIN_ENROLL_LABELS, DEFAULT_DEVICE_PIN_SETTINGS_LABELS, DEFAULT_DEVICE_PIN_UNLOCK_LABELS, DEFAULT_FORGOT_PASSWORD_FIELDS_LABELS, DEFAULT_FORGOT_PASSWORD_LABELS, DEFAULT_LOGIN_LABELS, DEFAULT_OTP_LABELS, DEFAULT_PASSKEY_LOGIN_LABELS, DEFAULT_PASSKEY_SETTINGS_LABELS, DEFAULT_PIN_LABELS, DEFAULT_PREFERRED_METHOD_HINT_LABELS, DEFAULT_PREFERRED_METHOD_SETTINGS_LABELS, DEFAULT_RESET_PASSWORD_LABELS, DEVICE_PIN_ALLOWED_DIGITS, DEVICE_PIN_DEFAULT_DIGITS, DevicePinEnrollErrorKey, DevicePinEnrollForm, DevicePinErrorKey, DevicePinInput, DevicePinLengthPicker, DevicePinOffer, DevicePinSettingsCard, DevicePinUnlockScreen, ForgotPasswordFields, ForgotPasswordForm, LoginForm, OtpForm, OtpLoginStep, PASSWORD_MAX_LENGTH, PASSWORD_MIN_LENGTH, PasskeyLoginButton, PasskeySettingsCard, PasswordPolicyError, PinForm, PreferredMethodHint, PreferredMethodSettingsCard, ResetPasswordError, ResetPasswordForm, cachePreferredMethod, clearCachedPreferredMethod, collectUserRoles, createBffAuthClient, createPreferencesClient, defaultAuthTheme, isAllowedPinDigits, isPasswordValid, isValidForgotPasswordEmail, readCachedPreferredMethod, readPasskeyError, readPasskeyRegistered, resolvePostLoginRoute, startPasskeyLogin, startPasskeyRegistration, syncPreferredMethodFromServer, useAuthTheme, useBffAuth, useBffForgotPassword, useBffLoginConfig, useBffResetPassword, useDevicePinDisable, useDevicePinEnroll, useDevicePinUnlock, useForgotPasswordSubmit, useOtpLogin, usePinLogin, usePreferredMethodSetting, useResetPasswordForm, validatePasswordPolicy, withTestIdPrefix };
2607
2923
  //# sourceMappingURL=index.mjs.map
2608
2924
  //# sourceMappingURL=index.mjs.map