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