@basictech/react 0.8.0-beta.2 → 0.8.0-beta.3

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
@@ -137,14 +137,37 @@ var init_syncProtocol = __esm({
137
137
  onError("Authentication failed: " + (err.message || err), RECONNECT_DELAY);
138
138
  }
139
139
  };
140
+ function handleVisibilityResume() {
141
+ if (document.visibilityState === "visible" && ws.readyState === WebSocket.OPEN) {
142
+ log("Page became visible - refreshing token for WebSocket");
143
+ resolveGetToken()({ forceRefresh: true }).then(function(newToken) {
144
+ if (ws.readyState === WebSocket.OPEN) {
145
+ ws.send(JSON.stringify({ type: "tokenUpdate", authToken: newToken }));
146
+ scheduleTokenRefresh(newToken);
147
+ }
148
+ }).catch(function(err) {
149
+ log("Token refresh on visibility resume failed:", err);
150
+ });
151
+ }
152
+ }
153
+ if (typeof document !== "undefined") {
154
+ document.addEventListener("visibilitychange", handleVisibilityResume);
155
+ }
156
+ function cleanupVisibilityListener() {
157
+ if (typeof document !== "undefined") {
158
+ document.removeEventListener("visibilitychange", handleVisibilityResume);
159
+ }
160
+ }
140
161
  ws.onerror = function(event) {
141
162
  clearRefreshTimer();
163
+ cleanupVisibilityListener();
142
164
  ws.close();
143
165
  log("ws.onerror", event);
144
166
  onError(event?.message, RECONNECT_DELAY);
145
167
  };
146
168
  ws.onclose = function(event) {
147
169
  clearRefreshTimer();
170
+ cleanupVisibilityListener();
148
171
  onError("Socket closed: " + event.reason, RECONNECT_DELAY);
149
172
  };
150
173
  var isFirstRound = true;
@@ -181,6 +204,7 @@ var init_syncProtocol = __esm({
181
204
  },
182
205
  disconnect: function() {
183
206
  clearRefreshTimer();
207
+ cleanupVisibilityListener();
184
208
  ws.close();
185
209
  }
186
210
  });
@@ -220,7 +244,7 @@ var init_syncProtocol = __esm({
220
244
  var version;
221
245
  var init_package = __esm({
222
246
  "package.json"() {
223
- version = "0.8.0-beta.2";
247
+ version = "0.8.0-beta.3";
224
248
  }
225
249
  });
226
250
 
@@ -305,7 +329,7 @@ function cleanOAuthParamsFromUrl() {
305
329
  const url = new URL(window.location.href);
306
330
  url.searchParams.delete("code");
307
331
  url.searchParams.delete("state");
308
- window.history.pushState({}, document.title, url.pathname + url.search);
332
+ window.history.replaceState({}, document.title, url.pathname + url.search);
309
333
  log("Cleaned OAuth parameters from URL");
310
334
  }
311
335
  }
@@ -1758,8 +1782,11 @@ var AuthManager = class {
1758
1782
  const refreshToken = await this.storage.get(STORAGE_KEYS.REFRESH_TOKEN);
1759
1783
  if (refreshToken) {
1760
1784
  log("Found refresh token in storage, attempting to refresh access token");
1761
- this.exchangeToken(refreshToken, true).catch((error) => {
1785
+ this.exchangeToken(refreshToken, true).catch(async (error) => {
1762
1786
  log("Error fetching refresh token:", error);
1787
+ if (this.isNetworkError(error)) {
1788
+ await this.restoreCachedUser();
1789
+ }
1763
1790
  });
1764
1791
  } else {
1765
1792
  const cachedUserInfo = await this.storage.get(STORAGE_KEYS.USER_INFO);
@@ -1828,7 +1855,8 @@ var AuthManager = class {
1828
1855
  log("Token refresh already in progress, waiting...");
1829
1856
  try {
1830
1857
  const newToken = await this.refreshPromise;
1831
- return newToken?.access_token || "";
1858
+ if (!newToken?.access_token) throw new Error("Token refresh returned empty access token");
1859
+ return newToken.access_token;
1832
1860
  } catch (error) {
1833
1861
  log("In-flight refresh failed:", error);
1834
1862
  if (this.isNetworkError(error)) {
@@ -1842,7 +1870,8 @@ var AuthManager = class {
1842
1870
  if (refreshToken) {
1843
1871
  try {
1844
1872
  const newToken = await this.exchangeToken(refreshToken, true);
1845
- return newToken?.access_token || "";
1873
+ if (!newToken?.access_token) throw new Error("Token refresh returned empty access token");
1874
+ return newToken.access_token;
1846
1875
  } catch (error) {
1847
1876
  log("Failed to refresh expired token:", error);
1848
1877
  if (this.isNetworkError(error)) {
@@ -1855,7 +1884,8 @@ var AuthManager = class {
1855
1884
  throw new Error("no refresh token available");
1856
1885
  }
1857
1886
  }
1858
- return this.token.access_token || "";
1887
+ if (!this.token.access_token) throw new Error("Token exists but access_token is empty");
1888
+ return this.token.access_token;
1859
1889
  }
1860
1890
  async getSignInUrl(redirectUri, endpoints) {
1861
1891
  log("getting sign in link...");
@@ -1864,7 +1894,7 @@ var AuthManager = class {
1864
1894
  }
1865
1895
  const pdsEndpoints = endpoints || this.defaultPdsEndpoints();
1866
1896
  await this.storage.set(STORAGE_KEYS.PDS_ENDPOINTS, JSON.stringify(pdsEndpoints));
1867
- const randomState = Math.random().toString(36).substring(6);
1897
+ const randomState = base64UrlEncode(crypto.getRandomValues(new Uint8Array(16)));
1868
1898
  await this.storage.set(STORAGE_KEYS.AUTH_STATE, randomState);
1869
1899
  const redirectUrl = redirectUri || window.location.href;
1870
1900
  if (!redirectUrl || !redirectUrl.startsWith("http://") && !redirectUrl.startsWith("https://")) {
@@ -1983,7 +2013,10 @@ var AuthManager = class {
1983
2013
  return requested.filter((s) => !granted.has(s));
1984
2014
  }
1985
2015
  /**
1986
- * Register online/offline handlers that retry pending refreshes.
2016
+ * Register online/offline and visibility handlers that retry pending
2017
+ * refreshes and proactively refresh tokens when the app resumes from
2018
+ * background (critical for PWAs and mobile browsers where timers are
2019
+ * frozen while backgrounded).
1987
2020
  * Returns a cleanup function for useEffect teardown.
1988
2021
  */
1989
2022
  setupNetworkListeners() {
@@ -2005,11 +2038,25 @@ var AuthManager = class {
2005
2038
  log("Network went offline");
2006
2039
  this.isOnline = false;
2007
2040
  };
2041
+ const handleVisibilityChange = () => {
2042
+ if (document.visibilityState === "visible" && this.isSignedIn) {
2043
+ log("App became visible - checking token freshness");
2044
+ this.getToken().catch((err) => {
2045
+ log("Token refresh on visibility resume failed:", err);
2046
+ });
2047
+ }
2048
+ };
2008
2049
  window.addEventListener("online", handleOnline);
2009
2050
  window.addEventListener("offline", handleOffline);
2051
+ if (typeof document !== "undefined") {
2052
+ document.addEventListener("visibilitychange", handleVisibilityChange);
2053
+ }
2010
2054
  return () => {
2011
2055
  window.removeEventListener("online", handleOnline);
2012
2056
  window.removeEventListener("offline", handleOffline);
2057
+ if (typeof document !== "undefined") {
2058
+ document.removeEventListener("visibilitychange", handleVisibilityChange);
2059
+ }
2013
2060
  };
2014
2061
  }
2015
2062
  // ------------------------------------------------------------------
@@ -2072,39 +2119,26 @@ var AuthManager = class {
2072
2119
  const decoded = jwtDecode(this.token.access_token);
2073
2120
  if (decoded.sub) this.did = decoded.sub;
2074
2121
  if (decoded.scope) this.tokenScope = decoded.scope;
2075
- const expirationBuffer = 5;
2076
- const isExpired = decoded.exp && decoded.exp < Date.now() / 1e3 + expirationBuffer;
2077
- if (isExpired) {
2078
- log("token is expired - refreshing ...");
2079
- const refreshToken = this.token.refresh_token;
2080
- if (!refreshToken) {
2081
- log("Error: No refresh token available for expired token");
2082
- this.isAuthReady = true;
2083
- this.notify();
2084
- return;
2085
- }
2086
- try {
2087
- const newToken = await this.exchangeToken(refreshToken, true);
2088
- await this.fetchUser(newToken?.access_token || "");
2089
- } catch (error) {
2090
- log("Failed to refresh token in processNewToken:", error);
2091
- if (this.isNetworkError(error)) {
2092
- log("Network issue - continuing with expired token until online");
2093
- await this.fetchUser(this.token.access_token);
2094
- } else {
2095
- this.isAuthReady = true;
2096
- this.notify();
2097
- }
2098
- }
2099
- } else {
2100
- await this.fetchUser(this.token.access_token);
2101
- }
2122
+ await this.fetchUser(this.token.access_token);
2102
2123
  } catch (error) {
2103
2124
  log("Error processing token:", error);
2104
2125
  this.isAuthReady = true;
2105
2126
  this.notify();
2106
2127
  }
2107
2128
  }
2129
+ async restoreCachedUser() {
2130
+ const cached = await this.storage.get(STORAGE_KEYS.USER_INFO);
2131
+ if (cached) {
2132
+ try {
2133
+ this.user = JSON.parse(cached);
2134
+ this.isSignedIn = true;
2135
+ log("Restored cached user info for offline mode");
2136
+ } catch {
2137
+ }
2138
+ }
2139
+ this.isAuthReady = true;
2140
+ this.notify();
2141
+ }
2108
2142
  async fetchUser(accessToken) {
2109
2143
  log("fetching user");
2110
2144
  try {
@@ -2138,8 +2172,12 @@ var AuthManager = class {
2138
2172
  this.notify();
2139
2173
  } catch (error) {
2140
2174
  log("Failed to fetch user info:", error);
2141
- this.isAuthReady = true;
2142
- this.notify();
2175
+ if (this.isNetworkError(error)) {
2176
+ await this.restoreCachedUser();
2177
+ } else {
2178
+ this.isAuthReady = true;
2179
+ this.notify();
2180
+ }
2143
2181
  }
2144
2182
  }
2145
2183
  /**
@@ -2235,9 +2273,12 @@ var AuthManager = class {
2235
2273
  this.pendingRefresh = true;
2236
2274
  throw new Error("Network issue - refresh will be retried when online");
2237
2275
  }
2238
- await this.clearStoredAuth();
2239
- this.resetAuthState();
2240
- this.notify();
2276
+ const definitiveErrors = ["invalid_grant", "invalid_client", "unauthorized_client"];
2277
+ if (typeof token.error === "string" && definitiveErrors.includes(token.error)) {
2278
+ await this.clearStoredAuth();
2279
+ this.resetAuthState();
2280
+ this.notify();
2281
+ }
2241
2282
  throw new Error(`Token refresh failed: ${token.error}`);
2242
2283
  } else {
2243
2284
  this.token = token;
@@ -2258,7 +2299,9 @@ var AuthManager = class {
2258
2299
  return token;
2259
2300
  } catch (error) {
2260
2301
  log("Token refresh error:", error);
2261
- if (!this.isNetworkError(error)) {
2302
+ const msg = error instanceof Error ? error.message : "";
2303
+ const alreadyHandled = msg.startsWith("Token refresh failed:");
2304
+ if (!alreadyHandled && !this.isNetworkError(error)) {
2262
2305
  await this.clearStoredAuth();
2263
2306
  this.resetAuthState();
2264
2307
  this.notify();
@@ -2302,6 +2345,7 @@ var AuthManager = class {
2302
2345
  await this.storage.remove(STORAGE_KEYS.PDS_ENDPOINTS);
2303
2346
  }
2304
2347
  isNetworkError(error) {
2348
+ if (error instanceof TypeError) return true;
2305
2349
  if (error instanceof Error) {
2306
2350
  return error.message.includes("offline") || error.message.includes("Network");
2307
2351
  }
@@ -2711,6 +2755,10 @@ function BasicProvider({
2711
2755
  debug,
2712
2756
  onAuthError: (error2) => {
2713
2757
  log("RemoteDB auth error:", error2);
2758
+ if (error2.errorType === "forbidden") {
2759
+ log("403 Forbidden - user lacks required scope, not signing out");
2760
+ return;
2761
+ }
2714
2762
  handleSignOut();
2715
2763
  }
2716
2764
  });