@dloizides/auth-web 1.4.0 → 1.5.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/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}`;
@@ -2527,6 +2549,217 @@ function PasskeySettingsCard({
2527
2549
  }
2528
2550
  );
2529
2551
  }
2552
+
2553
+ // src/preferences/preferredMethodCache.ts
2554
+ var CACHE_KEY = "dloizides.auth.preferredLoginMethod";
2555
+ function safeStorage() {
2556
+ try {
2557
+ if (typeof localStorage === "undefined") {
2558
+ return null;
2559
+ }
2560
+ return localStorage;
2561
+ } catch {
2562
+ return null;
2563
+ }
2564
+ }
2565
+ function readCachedPreferredMethod() {
2566
+ const store = safeStorage();
2567
+ if (store === null) {
2568
+ return null;
2569
+ }
2570
+ try {
2571
+ const value = store.getItem(CACHE_KEY);
2572
+ return value === null || value === "" ? null : value;
2573
+ } catch {
2574
+ return null;
2575
+ }
2576
+ }
2577
+ function cachePreferredMethod(method) {
2578
+ const store = safeStorage();
2579
+ if (store === null) {
2580
+ return;
2581
+ }
2582
+ try {
2583
+ if (method === null || method === "") {
2584
+ store.removeItem(CACHE_KEY);
2585
+ return;
2586
+ }
2587
+ store.setItem(CACHE_KEY, method);
2588
+ } catch {
2589
+ }
2590
+ }
2591
+ function clearCachedPreferredMethod() {
2592
+ cachePreferredMethod(null);
2593
+ }
2594
+
2595
+ // src/preferences/usePreferredMethodSetting.ts
2596
+ function usePreferredMethodSetting({
2597
+ client,
2598
+ onChanged
2599
+ }) {
2600
+ const [loading, setLoading] = react.useState(true);
2601
+ const [saving, setSaving] = react.useState(false);
2602
+ const [failed, setFailed] = react.useState(false);
2603
+ const [selected, setSelected] = react.useState(null);
2604
+ react.useEffect(() => {
2605
+ let active = true;
2606
+ client.getPreferredLoginMethod().then((method) => {
2607
+ if (active) {
2608
+ setSelected(method);
2609
+ }
2610
+ }).catch(() => {
2611
+ }).finally(() => {
2612
+ if (active) {
2613
+ setLoading(false);
2614
+ }
2615
+ });
2616
+ return () => {
2617
+ active = false;
2618
+ };
2619
+ }, [client]);
2620
+ const select = react.useCallback(
2621
+ (method) => {
2622
+ const previous = selected;
2623
+ if (method === previous) {
2624
+ return;
2625
+ }
2626
+ setFailed(false);
2627
+ setSaving(true);
2628
+ setSelected(method);
2629
+ client.setPreferredLoginMethod(method).then((ok) => {
2630
+ if (ok) {
2631
+ cachePreferredMethod(method);
2632
+ onChanged?.(method);
2633
+ return;
2634
+ }
2635
+ setSelected(previous);
2636
+ setFailed(true);
2637
+ }).catch(() => {
2638
+ setSelected(previous);
2639
+ setFailed(true);
2640
+ }).finally(() => {
2641
+ setSaving(false);
2642
+ });
2643
+ },
2644
+ [client, selected, onChanged]
2645
+ );
2646
+ return { loading, saving, failed, selected, select };
2647
+ }
2648
+ var NO_PREFERENCE = "__none__";
2649
+ function methodDisplayName(method, labels) {
2650
+ if (method === "password") {
2651
+ return labels.methodPassword;
2652
+ }
2653
+ if (method === "pin") {
2654
+ return labels.methodPin;
2655
+ }
2656
+ if (method === "passkey") {
2657
+ return labels.methodPasskey;
2658
+ }
2659
+ return method;
2660
+ }
2661
+ function PreferredMethodSettingsCard({
2662
+ client,
2663
+ availableMethods,
2664
+ theme: themeProp,
2665
+ labels: labelsProp,
2666
+ testIdPrefix,
2667
+ onChanged
2668
+ }) {
2669
+ const theme = useAuthTheme(themeProp);
2670
+ const styles3 = useAuthStyles(theme);
2671
+ const labels = react.useMemo(
2672
+ () => ({ ...DEFAULT_PREFERRED_METHOD_SETTINGS_LABELS, ...labelsProp }),
2673
+ [labelsProp]
2674
+ );
2675
+ const { loading, saving, failed, selected, select } = usePreferredMethodSetting({
2676
+ client,
2677
+ onChanged
2678
+ });
2679
+ const options = react.useMemo(
2680
+ () => [
2681
+ { value: null, key: NO_PREFERENCE, label: labels.noPreference },
2682
+ ...availableMethods.map((method) => ({
2683
+ value: method,
2684
+ key: method,
2685
+ label: methodDisplayName(method, labels)
2686
+ }))
2687
+ ],
2688
+ [availableMethods, labels]
2689
+ );
2690
+ return /* @__PURE__ */ jsxRuntime.jsxs(
2691
+ reactNative.View,
2692
+ {
2693
+ style: styles3.card,
2694
+ testID: withTestIdPrefix(AuthTestIds.preferredMethodSettings, testIdPrefix),
2695
+ children: [
2696
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles3.title, children: labels.title }),
2697
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles3.subtitle, children: labels.description }),
2698
+ loading ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.ActivityIndicator, { color: theme.colors.primary, size: "small" }) : options.map((option) => {
2699
+ const isSelected = option.value === selected;
2700
+ const optionTestId = `${withTestIdPrefix(
2701
+ AuthTestIds.preferredMethodSettingsOption,
2702
+ testIdPrefix
2703
+ )}-${option.key}`;
2704
+ return /* @__PURE__ */ jsxRuntime.jsx(
2705
+ reactNative.TouchableOpacity,
2706
+ {
2707
+ accessibilityHint: labels.description,
2708
+ accessibilityLabel: option.label,
2709
+ accessibilityRole: "radio",
2710
+ accessibilityState: { selected: isSelected },
2711
+ disabled: saving,
2712
+ style: isSelected ? styles3.secondaryButton : styles3.escapeLink,
2713
+ testID: optionTestId,
2714
+ onPress: () => select(option.value),
2715
+ children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: isSelected ? styles3.secondaryButtonText : styles3.linkText, children: isSelected ? `\u2713 ${option.label}` : option.label })
2716
+ },
2717
+ option.key
2718
+ );
2719
+ }),
2720
+ saving ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles3.helperText, children: labels.saving }) : null,
2721
+ failed ? /* @__PURE__ */ jsxRuntime.jsx(
2722
+ reactNative.Text,
2723
+ {
2724
+ style: styles3.errorText,
2725
+ testID: withTestIdPrefix(AuthTestIds.preferredMethodSettingsError, testIdPrefix),
2726
+ children: labels.saveFailed
2727
+ }
2728
+ ) : null
2729
+ ]
2730
+ }
2731
+ );
2732
+ }
2733
+ function hintFor(method, labels) {
2734
+ if (method === "password") {
2735
+ return labels.password;
2736
+ }
2737
+ if (method === "pin") {
2738
+ return labels.pin;
2739
+ }
2740
+ if (method === "passkey") {
2741
+ return labels.passkey;
2742
+ }
2743
+ return null;
2744
+ }
2745
+ function PreferredMethodHint({
2746
+ method,
2747
+ theme: themeProp,
2748
+ labels: labelsProp,
2749
+ testIdPrefix
2750
+ }) {
2751
+ const theme = useAuthTheme(themeProp);
2752
+ const styles3 = useAuthStyles(theme);
2753
+ const labels = react.useMemo(
2754
+ () => ({ ...DEFAULT_PREFERRED_METHOD_HINT_LABELS, ...labelsProp }),
2755
+ [labelsProp]
2756
+ );
2757
+ const text = hintFor(method, labels);
2758
+ if (text === null) {
2759
+ return null;
2760
+ }
2761
+ return /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { testID: withTestIdPrefix(AuthTestIds.preferredMethodHint, testIdPrefix), children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles3.helperText, children: text }) });
2762
+ }
2530
2763
  var EMPTY_CONFIG = {
2531
2764
  methods: [],
2532
2765
  registrationEnabled: false,
@@ -2561,6 +2794,77 @@ function useBffLoginConfig(client) {
2561
2794
  }, [client]);
2562
2795
  return { config, loading };
2563
2796
  }
2797
+
2798
+ // src/preferences/preferencesClient.ts
2799
+ var CSRF_HEADER = "X-BFF-Csrf";
2800
+ var CSRF_HEADER_VALUE = "1";
2801
+ var JSON_CONTENT_TYPE = "application/json";
2802
+ var DEFAULT_TENANTS_SEGMENT = "tenants";
2803
+ var PREFERENCES_PATH = "api/v1/me/preferences";
2804
+ var LOGIN_METHOD_PREFERENCE_PATH = "api/v1/me/login-method-preference";
2805
+ function isRecord(value) {
2806
+ return typeof value === "object" && value !== null;
2807
+ }
2808
+ function readOptionalString(record, key) {
2809
+ const value = record[key];
2810
+ if (typeof value !== "string" || value === "") {
2811
+ return null;
2812
+ }
2813
+ return value;
2814
+ }
2815
+ function createPreferencesClient(options) {
2816
+ const http = options.http;
2817
+ const baseUrl = (options.baseUrl ?? "").replace(/\/$/, "");
2818
+ const segment = options.tenantsSegment ?? DEFAULT_TENANTS_SEGMENT;
2819
+ const proxyBase = `${baseUrl}/bff/api/${segment}`;
2820
+ return {
2821
+ async getPreferredLoginMethod() {
2822
+ try {
2823
+ const response = await http({
2824
+ url: `${proxyBase}/${PREFERENCES_PATH}`,
2825
+ method: "GET",
2826
+ headers: { Accept: JSON_CONTENT_TYPE },
2827
+ credentials: "include"
2828
+ });
2829
+ if (!response.ok || !isRecord(response.data)) {
2830
+ return null;
2831
+ }
2832
+ return readOptionalString(response.data, "preferredLoginMethod");
2833
+ } catch {
2834
+ return null;
2835
+ }
2836
+ },
2837
+ async setPreferredLoginMethod(method) {
2838
+ try {
2839
+ const response = await http({
2840
+ url: `${proxyBase}/${LOGIN_METHOD_PREFERENCE_PATH}`,
2841
+ method: "PUT",
2842
+ headers: {
2843
+ "Content-Type": JSON_CONTENT_TYPE,
2844
+ Accept: JSON_CONTENT_TYPE,
2845
+ [CSRF_HEADER]: CSRF_HEADER_VALUE
2846
+ },
2847
+ body: JSON.stringify({ method }),
2848
+ credentials: "include"
2849
+ });
2850
+ return response.ok;
2851
+ } catch {
2852
+ return false;
2853
+ }
2854
+ }
2855
+ };
2856
+ }
2857
+
2858
+ // src/preferences/syncPreferredMethodCache.ts
2859
+ async function syncPreferredMethodFromServer(client) {
2860
+ try {
2861
+ const method = await client.getPreferredLoginMethod();
2862
+ cachePreferredMethod(method);
2863
+ return method;
2864
+ } catch {
2865
+ return null;
2866
+ }
2867
+ }
2564
2868
  async function lazyFetchHttpClient(request) {
2565
2869
  const fetchImpl = typeof fetch === "function" ? fetch.bind(globalThis) : void 0;
2566
2870
  if (fetchImpl === void 0) {
@@ -2576,11 +2880,11 @@ function createBffAuthClient(options = {}) {
2576
2880
  }
2577
2881
 
2578
2882
  // src/router/resolvePostLoginRoute.ts
2579
- function isRecord(value) {
2883
+ function isRecord2(value) {
2580
2884
  return typeof value === "object" && value !== null;
2581
2885
  }
2582
2886
  function readRealmAccessRoles(realmAccess) {
2583
- if (!isRecord(realmAccess)) {
2887
+ if (!isRecord2(realmAccess)) {
2584
2888
  return [];
2585
2889
  }
2586
2890
  const roles = realmAccess.roles;
@@ -2625,6 +2929,8 @@ exports.DEFAULT_OTP_LABELS = DEFAULT_OTP_LABELS;
2625
2929
  exports.DEFAULT_PASSKEY_LOGIN_LABELS = DEFAULT_PASSKEY_LOGIN_LABELS;
2626
2930
  exports.DEFAULT_PASSKEY_SETTINGS_LABELS = DEFAULT_PASSKEY_SETTINGS_LABELS;
2627
2931
  exports.DEFAULT_PIN_LABELS = DEFAULT_PIN_LABELS;
2932
+ exports.DEFAULT_PREFERRED_METHOD_HINT_LABELS = DEFAULT_PREFERRED_METHOD_HINT_LABELS;
2933
+ exports.DEFAULT_PREFERRED_METHOD_SETTINGS_LABELS = DEFAULT_PREFERRED_METHOD_SETTINGS_LABELS;
2628
2934
  exports.DEFAULT_RESET_PASSWORD_LABELS = DEFAULT_RESET_PASSWORD_LABELS;
2629
2935
  exports.DEVICE_PIN_ALLOWED_DIGITS = DEVICE_PIN_ALLOWED_DIGITS;
2630
2936
  exports.DEVICE_PIN_DEFAULT_DIGITS = DEVICE_PIN_DEFAULT_DIGITS;
@@ -2647,19 +2953,26 @@ exports.PasskeyLoginButton = PasskeyLoginButton;
2647
2953
  exports.PasskeySettingsCard = PasskeySettingsCard;
2648
2954
  exports.PasswordPolicyError = PasswordPolicyError;
2649
2955
  exports.PinForm = PinForm;
2956
+ exports.PreferredMethodHint = PreferredMethodHint;
2957
+ exports.PreferredMethodSettingsCard = PreferredMethodSettingsCard;
2650
2958
  exports.ResetPasswordError = ResetPasswordError;
2651
2959
  exports.ResetPasswordForm = ResetPasswordForm;
2960
+ exports.cachePreferredMethod = cachePreferredMethod;
2961
+ exports.clearCachedPreferredMethod = clearCachedPreferredMethod;
2652
2962
  exports.collectUserRoles = collectUserRoles;
2653
2963
  exports.createBffAuthClient = createBffAuthClient;
2964
+ exports.createPreferencesClient = createPreferencesClient;
2654
2965
  exports.defaultAuthTheme = defaultAuthTheme;
2655
2966
  exports.isAllowedPinDigits = isAllowedPinDigits;
2656
2967
  exports.isPasswordValid = isPasswordValid;
2657
2968
  exports.isValidForgotPasswordEmail = isValidForgotPasswordEmail;
2969
+ exports.readCachedPreferredMethod = readCachedPreferredMethod;
2658
2970
  exports.readPasskeyError = readPasskeyError;
2659
2971
  exports.readPasskeyRegistered = readPasskeyRegistered;
2660
2972
  exports.resolvePostLoginRoute = resolvePostLoginRoute;
2661
2973
  exports.startPasskeyLogin = startPasskeyLogin;
2662
2974
  exports.startPasskeyRegistration = startPasskeyRegistration;
2975
+ exports.syncPreferredMethodFromServer = syncPreferredMethodFromServer;
2663
2976
  exports.useAuthTheme = useAuthTheme;
2664
2977
  exports.useBffAuth = useBffAuth;
2665
2978
  exports.useBffForgotPassword = useBffForgotPassword;
@@ -2671,6 +2984,7 @@ exports.useDevicePinUnlock = useDevicePinUnlock;
2671
2984
  exports.useForgotPasswordSubmit = useForgotPasswordSubmit;
2672
2985
  exports.useOtpLogin = useOtpLogin;
2673
2986
  exports.usePinLogin = usePinLogin;
2987
+ exports.usePreferredMethodSetting = usePreferredMethodSetting;
2674
2988
  exports.useResetPasswordForm = useResetPasswordForm;
2675
2989
  exports.validatePasswordPolicy = validatePasswordPolicy;
2676
2990
  exports.withTestIdPrefix = withTestIdPrefix;