@ciromaciel/auth-react 1.2.0 → 1.3.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
@@ -112,6 +112,28 @@ function forgetAccount(email) {
112
112
  return next;
113
113
  }
114
114
 
115
+ /**
116
+ * Replaces the local copy with the list the worker returned.
117
+ *
118
+ * The worker's list is the one every panel shares, so when it has accounts it
119
+ * wins: an account removed on another panel must not come back from this
120
+ * panel's stale copy. An EMPTY answer does not wipe the local one — it is what
121
+ * a browser that signed in before the shared list existed gets, and those
122
+ * shortcuts are still true.
123
+ *
124
+ * @returns the list to show
125
+ */
126
+ function adoptRecentAccounts(remote) {
127
+ if (!Array.isArray(remote) || remote.length === 0) return listRecentAccounts();
128
+ const next = remote.filter(account => account && typeof account.email === 'string' && account.email.includes('@')).map(account => ({
129
+ email: normalizeEmail(account.email),
130
+ method: typeof account.method === 'string' && account.method ? account.method : 'code',
131
+ lastUsedAt: Number(account.lastUsedAt) || 0
132
+ })).sort((a, b) => b.lastUsedAt - a.lastUsedAt).slice(0, MAX_RECENT_ACCOUNTS);
133
+ writeRecentAccounts(next);
134
+ return next;
135
+ }
136
+
115
137
  /** Records the provider this tab is leaving for. */
116
138
  function markSocialDeparture(provider) {
117
139
  try {
@@ -693,6 +715,64 @@ const updateProfile = async data => {
693
715
  });
694
716
  };
695
717
 
718
+ /*--- Recent accounts ------------------------------------------------------*/
719
+
720
+ /*
721
+ * The server's copy of this browser's sign-in shortcuts.
722
+ *
723
+ * `localStorage` is per origin, so a list kept only there stayed on the panel
724
+ * where the person signed in. The worker keeps it in an HttpOnly cookie on its
725
+ * own host, which every panel of the application reaches — and answers only to
726
+ * the origins the application allows (`routes/recent-accounts.js`).
727
+ *
728
+ * All three fail soft: the shortcuts are a convenience, and a network error or
729
+ * an origin the worker refuses must never cost the sign-in. `null` means "no
730
+ * answer", which the screen reads as "keep what you have".
731
+ */
732
+
733
+ /** The list for this application, or `null` when the worker did not answer. */
734
+ const fetchRecentAccounts = async () => {
735
+ try {
736
+ const response = await api('/auth/recent-accounts');
737
+ return Array.isArray(response?.items) ? response.items : null;
738
+ } catch {
739
+ return null;
740
+ }
741
+ };
742
+
743
+ /**
744
+ * Records the account of the CURRENT session. The worker reads the email from
745
+ * the session, never from here. `keepalive` because the screen is usually
746
+ * navigating away at this very moment.
747
+ */
748
+ const saveRecentAccount = async (method = 'code') => {
749
+ try {
750
+ const response = await api('/auth/recent-accounts', {
751
+ method: 'POST',
752
+ body: JSON.stringify({
753
+ method
754
+ }),
755
+ keepalive: true
756
+ });
757
+ return Array.isArray(response?.items) ? response.items : null;
758
+ } catch {
759
+ return null;
760
+ }
761
+ };
762
+
763
+ /** Takes one account off this browser's list, on every panel. */
764
+ const deleteRecentAccount = async email => {
765
+ try {
766
+ await api(`/auth/recent-accounts/${encodeURIComponent(email)}`, {
767
+ method: 'DELETE',
768
+ keepalive: true
769
+ });
770
+ return true;
771
+ } catch {
772
+ return false;
773
+ }
774
+ };
775
+
696
776
  /*--- Social sign-in -------------------------------------------------------*/
697
777
 
698
778
  /**
@@ -801,6 +881,9 @@ const consumeSocialToken = () => {
801
881
  if (provider) {
802
882
  const email = decodeJWT(token)?.email;
803
883
  if (email) rememberAccount(email, provider);
884
+ // The shared copy, so the other panels learn it too. Not awaited: the
885
+ // token is already stored, and the sign-in must not wait on a shortcut.
886
+ saveRecentAccount(provider);
804
887
  }
805
888
  params.delete('token');
806
889
  const rest = params.toString();
@@ -2784,6 +2867,32 @@ function SignIn({
2784
2867
  // eslint-disable-next-line react-hooks/exhaustive-deps -- redirectOrigins enters through the serialized key above
2785
2868
  }, [authLoading, user, authenticatedRedirect, handleRedirect, redirectOriginsKey, navigate]);
2786
2869
 
2870
+ /*
2871
+ * The shared list, from the worker.
2872
+ *
2873
+ * The local copy renders at once; this replaces it when the answer
2874
+ * arrives. That is what makes an account used on the Auth panel appear
2875
+ * on Hoster: `localStorage` never crosses between the two origins.
2876
+ *
2877
+ * If the person has already started typing an email, the list does not
2878
+ * yank the form away from under them — it only feeds the "Contas salvas"
2879
+ * link, one click away.
2880
+ */
2881
+ react.useEffect(() => {
2882
+ if (!recentAccounts) return;
2883
+ let isActive = true;
2884
+ fetchRecentAccounts().then(remote => {
2885
+ if (!isActive || remote === null) return;
2886
+ const next = adoptRecentAccounts(remote);
2887
+ if (form$1.isDirty()) setIsChoosingOther(true);
2888
+ setAccounts(next);
2889
+ });
2890
+ return () => {
2891
+ isActive = false;
2892
+ };
2893
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- once per mount, like the local read
2894
+ }, [recentAccounts]);
2895
+
2787
2896
  // Step 1 — ask for the code.
2788
2897
  const handleRequest = async values => {
2789
2898
  if (sending) return false;
@@ -2843,6 +2952,9 @@ function SignIn({
2843
2952
  const handleForget = account => {
2844
2953
  const next = forgetAccount(account.email);
2845
2954
  setAccounts(next);
2955
+ // On every panel, not just this one. The local removal above already
2956
+ // took it off this screen, so a failure here costs nothing visible.
2957
+ deleteRecentAccount(account.email);
2846
2958
  if (next.length === 0) setIsManaging(false);
2847
2959
  };
2848
2960
 
@@ -2871,7 +2983,10 @@ function SignIn({
2871
2983
  // Only now, with a session: an email that never received a valid
2872
2984
  // code never becomes a suggestion. Written before the redirect,
2873
2985
  // which may unmount this screen.
2874
- if (recentAccounts) setAccounts(rememberAccount(sentTo, 'code'));
2986
+ if (recentAccounts) {
2987
+ setAccounts(rememberAccount(sentTo, 'code'));
2988
+ saveRecentAccount('code');
2989
+ }
2875
2990
  const target = handleRedirect ? getRedirectFromLocation(redirectOrigins) : null;
2876
2991
  if (target) applyRedirect(target, navigate);
2877
2992
  onSuccess?.(result?.user ?? null, {
@@ -4121,6 +4236,7 @@ exports.TOKEN_STORAGE_KEY = TOKEN_STORAGE_KEY;
4121
4236
  exports.UserInformation = UserInformation;
4122
4237
  exports.UserProfile = UserProfile;
4123
4238
  exports.Wordmark = Wordmark;
4239
+ exports.adoptRecentAccounts = adoptRecentAccounts;
4124
4240
  exports.announceIdentityChange = announceIdentityChange;
4125
4241
  exports.applyRedirect = applyRedirect;
4126
4242
  exports.clearIdentitySwitching = clearIdentitySwitching;
@@ -4128,7 +4244,9 @@ exports.configure = configure;
4128
4244
  exports.consumeSocialError = consumeSocialError;
4129
4245
  exports.consumeSocialToken = consumeSocialToken;
4130
4246
  exports.decodeJWT = decodeJWT;
4247
+ exports.deleteRecentAccount = deleteRecentAccount;
4131
4248
  exports.endImpersonation = endImpersonation;
4249
+ exports.fetchRecentAccounts = fetchRecentAccounts;
4132
4250
  exports.forgetAccount = forgetAccount;
4133
4251
  exports.getApiUrl = getApiUrl;
4134
4252
  exports.getApplicationInfo = getApplicationInfo;
@@ -4150,6 +4268,7 @@ exports.requestCode = requestCode;
4150
4268
  exports.resolveRedirect = resolveRedirect;
4151
4269
  exports.revokeOtherSessions = revokeOtherSessions;
4152
4270
  exports.revokeSession = revokeSession;
4271
+ exports.saveRecentAccount = saveRecentAccount;
4153
4272
  exports.setStoredToken = setStoredToken;
4154
4273
  exports.shouldSignOutOn401 = shouldSignOutOn401;
4155
4274
  exports.signOut = signOut;